Android學習指南 — Android進階篇

好了,本次的技術分享到這里就告一段落了,如果嫌博客麻煩,可看主頁簡介,找我拿PDF版本的哦

Java基礎篇

Android基礎篇(一)

Android基礎篇(二)

ART

ART 代表 Android Runtime,其處理應用程序執行的方式完全不同于 Dalvik,Dalvik 是依靠一個 Just-In-Time (JIT) 編譯器去解釋字節碼。開發者編譯后的應用代碼需要通過一個解釋器在用戶的設備上運行,這一機制并不高效,但讓應用能更容易在不同硬件和架構上運 行。ART 則完全改變了這套做法,在應用安裝時就預編譯字節碼到機器語言,這一機制叫 Ahead-Of-Time (AOT)編譯。在移除解釋代碼這一過程后,應用程序執行將更有效率,啟動更快。

ART 功能

預先 (AOT) 編譯

ART 引入了預先編譯機制,可提高應用的性能。ART 還具有比 Dalvik 更嚴格的安裝時驗證。在安裝時,ART 使用設備自帶的 dex2oat 工具來編譯應用。該實用工具接受 DEX 文件作為輸入,并為目標設備生成經過編譯的應用可執行文件。該工具應能夠順利編譯所有有效的 DEX 文件。

垃圾回收優化

垃圾回收 (GC) 可能有損于應用性能,從而導致顯示不穩定、界面響應速度緩慢以及其他問題。ART 通過以下幾種方式對垃圾回收做了優化:

  • 只有一次(而非兩次)GC 暫停
  • 在 GC 保持暫停狀態期間并行處理
  • 在清理最近分配的短時對象這種特殊情況中,回收器的總 GC 時間更短
  • 優化了垃圾回收的工效,能夠更加及時地進行并行垃圾回收,這使得 GC_FOR_ALLOC 事件在典型用例中極為罕見
  • 壓縮 GC 以減少后臺內存使用和碎片

開發和調試方面的優化

  • 支持采樣分析器

一直以來,開發者都使用 Traceview 工具(用于跟蹤應用執行情況)作為分析器。雖然 Traceview 可提供有用的信息,但每次方法調用產生的開銷會導致 Dalvik 分析結果出現偏差,而且使用該工具明顯會影響運行時性能

ART 添加了對沒有這些限制的專用采樣分析器的支持,因而可更準確地了解應用執行情況,而不會明顯減慢速度。KitKat 版本為 Dalvik 的 Traceview 添加了采樣支持。

  • 支持更多調試功能

ART 支持許多新的調試選項,特別是與監控和垃圾回收相關的功能。例如,查看堆棧跟蹤中保留了哪些鎖,然后跳轉到持有鎖的線程;詢問指定類的當前活動的實例數、請求查看實例,以及查看使對象保持有效狀態的參考;過濾特定實例的事件(如斷點)等。

  • 優化了異常和崩潰報告中的診斷詳細信息

當發生運行時異常時,ART 會為您提供盡可能多的上下文和詳細信息。ART 會提供 java.lang.ClassCastException、java.lang.ClassNotFoundExceptionjava.lang.NullPointerException 的更多異常詳細信息(較高版本的 Dalvik 會提供 java.lang.ArrayIndexOutOfBoundsExceptionjava.lang.ArrayStoreException 的更多異常詳細信息,這些信息現在包括數組大小和越界偏移量;ART 也提供這類信息)。

ART GC

ART 有多個不同的 GC 方案,這些方案包括運行不同垃圾回收器。默認方案是 CMS(并發標記清除)方案,主要使用粘性 CMS 和部分 CMS。粘性 CMS 是 ART 的不移動分代垃圾回收器。它僅掃描堆中自上次 GC 后修改的部分,并且只能回收自上次 GC 后分配的對象。除 CMS 方案外,當應用將進程狀態更改為察覺不到卡頓的進程狀態(例如,后臺或緩存)時,ART 將執行堆壓縮。

除了新的垃圾回收器之外,ART 還引入了一種基于位圖的新內存分配程序,稱為 RosAlloc(插槽運行分配器)。此新分配器具有分片鎖,當分配規模較小時可添加線程的本地緩沖區,因而性能優于 DlMalloc。

與 Dalvik 相比,ART CMS 垃圾回收計劃在很多方面都有一定的改善:

  • 與 Dalvik 相比,暫停次數從 2 次減少到 1 次。Dalvik 的第一次暫停主要是為了進行根標記,即在 ART 中進行并發標記,讓線程標記自己的根,然后馬上恢復運行。
  • 與 Dalvik 類似,ART GC 在清除過程開始之前也會暫停 1 次。兩者在這方面的主要差異在于:在此暫停期間,某些 Dalvik 環節在 ART 中并發進行。這些環節包括 java.lang.ref.Reference 處理、系統弱清除(例如,jni 弱全局等)、重新標記非線程根和卡片預清理。在 ART 暫停期間仍進行的階段包括掃描臟卡片以及重新標記線程根,這些操作有助于縮短暫停時間。
  • 相對于 Dalvik,ART GC 改進的最后一個方面是粘性 CMS 回收器增加了 GC 吞吐量。不同于普通的分代 GC,粘性 CMS 不移動。系統會將年輕對象保存在一個分配堆棧(基本上是 java.lang.Object 數組)中,而非為其設置一個專屬區域。這樣可以避免移動所需的對象以維持低暫停次數,但缺點是容易在堆棧中加入大量復雜對象圖像而使堆棧變長。

ART GC 與 Dalvik 的另一個主要區別在于 ART GC 引入了移動垃圾回收器。使用移動 GC 的目的在于通過堆壓縮來減少后臺應用使用的內存。目前,觸發堆壓縮的事件是 ActivityManager 進程狀態的改變。當應用轉到后臺運行時,它會通知 ART 已進入不再“感知”卡頓的進程狀態。此時 ART 會進行一些操作(例如,壓縮和監視器壓縮),從而導致應用線程長時間暫停。目前正在使用的兩個移動 GC 是同構空間壓縮和半空間壓縮。

  • 半空間壓縮將對象在兩個緊密排列的碰撞指針空間之間進行移動。這種移動 GC 適用于小內存設備,因為它可以比同構空間壓縮稍微多節省一點內存。額外節省出的空間主要來自緊密排列的對象,這樣可以避免 RosAlloc/DlMalloc 分配器占用開銷。由于 CMS 仍在前臺使用,且不能從碰撞指針空間中進行收集,因此當應用在前臺使用時,半空間還要再進行一次轉換。這種情況并不理想,因為它可能引起較長時間的暫停。
  • 同構空間壓縮通過將對象從一個 RosAlloc 空間復制到另一個 RosAlloc 空間來實現。這有助于通過減少堆碎片來減少內存使用量。這是目前非低內存設備的默認壓縮模式。相比半空間壓縮,同構空間壓縮的主要優勢在于應用從后臺切換到前臺時無需進行堆轉換。

Hook

基本流程

1、根據需求確定 要 hook 的對象
2、尋找要hook的對象的持有者,拿到要 hook 的對象
3、定義“要 hook 的對象”的代理類,并且創建該類的對象
4、使用上一步創建出來的對象,替換掉要 hook 的對象

使用示例

/**
* hook的核心代碼
* 這個方法的唯一目的:用自己的點擊事件,替換掉 View 原來的點擊事件
*
* @param view hook的范圍僅限于這個view
*/
@SuppressLint({"DiscouragedPrivateApi", "PrivateApi"})
public static void hook(Context context, final View view) {//
    try {
        // 反射執行View類的getListenerInfo()方法,拿到v的mListenerInfo對象,這個對象就是點擊事件的持有者
        Method method = View.class.getDeclaredMethod("getListenerInfo");
        method.setAccessible(true);//由于getListenerInfo()方法并不是public的,所以要加這個代碼來保證訪問權限
        Object mListenerInfo = method.invoke(view);//這里拿到的就是mListenerInfo對象,也就是點擊事件的持有者

        // 要從這里面拿到當前的點擊事件對象
        Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");// 這是內部類的表示方法
        Field field = listenerInfoClz.getDeclaredField("mOnClickListener");
        final View.OnClickListener onClickListenerInstance = (View.OnClickListener) field.get(mListenerInfo);//取得真實的mOnClickListener對象

        // 2\. 創建我們自己的點擊事件代理類
        //   方式1:自己創建代理類
        //   ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance);
        //   方式2:由于View.OnClickListener是一個接口,所以可以直接用動態代理模式
        // Proxy.newProxyInstance的3個參數依次分別是:
        // 本地的類加載器;
        // 代理類的對象所繼承的接口(用Class數組表示,支持多個接口)
        // 代理類的實際邏輯,封裝在new出來的InvocationHandler內
        Object proxyOnClickListener = Proxy.newProxyInstance(context.getClass().getClassLoader(), new Class[]{View.OnClickListener.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Log.d("HookSetOnClickListener", "點擊事件被hook到了");//加入自己的邏輯
                return method.invoke(onClickListenerInstance, args);//執行被代理的對象的邏輯
            }
        });
        // 3\. 用我們自己的點擊事件代理類,設置到"持有者"中
        field.set(mListenerInfo, proxyOnClickListener);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 自定義代理類
static class ProxyOnClickListener implements View.OnClickListener {
    View.OnClickListener oriLis;

    public ProxyOnClickListener(View.OnClickListener oriLis) {
        this.oriLis = oriLis;
    }

    @Override
    public void onClick(View v) {
        Log.d("HookSetOnClickListener", "點擊事件被hook到了");
        if (oriLis != null) {
            oriLis.onClick(v);
        }
    }
}
復制代碼

Proguard

Proguard 具有以下三個功能:

  • 壓縮(Shrink): 檢測和刪除沒有使用的類,字段,方法和特性
  • 優化(Optimize) : 分析和優化Java字節碼
  • 混淆(Obfuscate): 使用簡短的無意義的名稱,對類,字段和方法進行重命名

規則

  • 關鍵字
關鍵字 描述
keep 保留類和類中的成員,防止被混淆或移除
keepnames 保留類和類中的成員,防止被混淆,成員沒有被引用會被移除
keepclassmembers 只保留類中的成員,防止被混淆或移除
keepclassmembernames 只保留類中的成員,防止被混淆,成員沒有引用會被移除
keepclasseswithmembers 保留類和類中的成員,防止被混淆或移除,保留指明的成員
keepclasseswithmembernames 保留類和類中的成員,防止被混淆,保留指明的成員,成員沒有引用會被移除
  • 通配符
通配符 描述
匹配類中的所有字段
匹配類中所有的方法
匹配類中所有的構造函數
* 匹配任意長度字符,不包含包名分隔符(.)
** 匹配任意長度字符,包含包名分隔符(.)
*** 匹配任意參數類型
  • 指定混淆時可使用字典
-applymapping filename 指定重用一個已經寫好了的map文件作為新舊元素名的映射。
-obfuscationdictionary filename 指定一個文本文件用來生成混淆后的名字。
-classobfuscationdictionary filename 指定一個混淆類名的字典
-packageobfuscationdictionary filename 指定一個混淆包名的字典
-overloadaggressively 混淆的時候大量使用重載,多個方法名使用同一個混淆名(慎用)
復制代碼

公共模板

#############################################
#
# 對于一些基本指令的添加
#
#############################################
# 代碼混淆壓縮比,在 0~7 之間,默認為 5,一般不做修改
-optimizationpasses 5

# 混合時不使用大小寫混合,混合后的類名為小寫
-dontusemixedcaseclassnames

# 指定不去忽略非公共庫的類
-dontskipnonpubliclibraryclasses

# 這句話能夠使我們的項目混淆后產生映射文件
# 包含有類名->混淆后類名的映射關系
-verbose

# 指定不去忽略非公共庫的類成員
-dontskipnonpubliclibraryclassmembers

# 不做預校驗,preverify 是 proguard 的四個步驟之一,Android 不需要 preverify,去掉這一步能夠加快混淆速度。
-dontpreverify

# 保留 Annotation 不混淆
-keepattributes *Annotation*,InnerClasses

# 避免混淆泛型
-keepattributes Signature

# 拋出異常時保留代碼行號
-keepattributes SourceFile,LineNumberTable

# 指定混淆是采用的算法,后面的參數是一個過濾器
# 這個過濾器是谷歌推薦的算法,一般不做更改
-optimizations !code/simplification/cast,!field/*,!class/merging/*

#############################################
#
# Android開發中一些需要保留的公共部分
#
#############################################

# 保留我們使用的四大組件,自定義的 Application 等等這些類不被混淆
# 因為這些子類都有可能被外部調用
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService

# 保留 support 下的所有類及其內部類
-keep class android.support.** { *; }

# 保留繼承的
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**

# 保留 R 下面的資源
-keep class **.R$* { *; }

# 保留本地 native 方法不被混淆
-keepclasseswithmembernames class * {
    native <methods>;
}

# 保留在 Activity 中的方法參數是view的方法,
# 這樣以來我們在 layout 中寫的 onClick 就不會被影響
-keepclassmembers class * extends android.app.Activity {
    public void *(android.view.View);
}

# 保留枚舉類不被混淆
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# 保留我們自定義控件(繼承自 View)不被混淆
-keep public class * extends android.view.View {
    *** get*();
    void set*(***);
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
}

# 保留 Parcelable 序列化類不被混淆
-keep class * implements android.os.Parcelable {
    public static final android.os.Parcelable$Creator *;
}

# 保留 Serializable 序列化的類不被混淆
-keepnames class * implements java.io.Serializable
-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    !static !transient <fields>;
    !private <fields>;
    !private <methods>;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

# 對于帶有回調函數的 onXXEvent、**On*Listener 的,不能被混淆
-keepclassmembers class * {
    void *(**On*Event);
    void *(**On*Listener);
}

# webView 處理,項目中沒有使用到 webView 忽略即可
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
    public *;
}
-keepclassmembers class * extends android.webkit.webViewClient {
    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
    public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.webViewClient {
    public void *(android.webkit.webView, java.lang.String);
}

# js
-keepattributes JavascriptInterface
-keep class android.webkit.JavascriptInterface { *; }
-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}

# @Keep
-keep,allowobfuscation @interface android.support.annotation.Keep
-keep @android.support.annotation.Keep class *
-keepclassmembers class * {
    @android.support.annotation.Keep *;
}
復制代碼

常用的自定義混淆規則

# 通配符*,匹配任意長度字符,但不含包名分隔符(.)
# 通配符**,匹配任意長度字符,并且包含包名分隔符(.)

# 不混淆某個類
-keep public class com.jasonwu.demo.Test { *; }

# 不混淆某個包所有的類
-keep class com.jasonwu.demo.test.** { *; }

# 不混淆某個類的子類
-keep public class * com.jasonwu.demo.Test { *; }

# 不混淆所有類名中包含了 ``model`` 的類及其成員
-keep public class **.*model*.** {*;}

# 不混淆某個接口的實現
-keep class * implements com.jasonwu.demo.TestInterface { *; }

# 不混淆某個類的構造方法
-keepclassmembers class com.jasonwu.demo.Test { 
  public <init>(); 
}

# 不混淆某個類的特定的方法
-keepclassmembers class com.jasonwu.demo.Test { 
  public void test(java.lang.String); 
}
復制代碼

aar中增加獨立的混淆配置

build.gralde

android {
    ···
    defaultConfig {
        ···
        consumerProguardFile 'proguard-rules.pro'
    }
    ···
}
復制代碼

檢查混淆和追蹤異常

開啟 Proguard 功能,則每次構建時 ProGuard 都會輸出下列文件:

  • dump.txt
    說明 APK 中所有類文件的內部結構。
  • mapping.txt
    提供原始與混淆過的類、方法和字段名稱之間的轉換。
  • seeds.txt
    列出未進行混淆的類和成員。
  • usage.txt
    列出從 APK 移除的代碼。

這些文件保存在 /build/outputs/mapping/release/ 中。我們可以查看 seeds.txt 里面是否是我們需要保留的,以及 usage.txt 里查看是否有誤刪除的代碼。 mapping.txt 文件很重要,由于我們的部分代碼是經過重命名的,如果該部分出現 bug,對應的異常堆棧信息里的類或成員也是經過重命名的,難以定位問題。我們可以用 retrace 腳本(在 Windows 上為 retrace.bat;在 Mac/Linux 上為 retrace.sh)。它位于 /tools/proguard/ 目錄中。該腳本利用 mapping.txt 文件和你的異常堆棧文件生成沒有經過混淆的異常堆棧文件,這樣就可以看清是哪里出問題了。使用 retrace 工具的語法如下:

retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]
復制代碼

架構

MVC

在 Android 中,三者的關系如下:

由于在 Android 中 xml 布局的功能性太弱,所以 Activity 承擔了絕大部分的工作,所以在 Android 中 mvc 更像:

總結:

  • 具有一定的分層,model 解耦,controller 和 view 并沒有解耦
  • controller 和 view 在 Android 中無法做到徹底分離,Controller 變得臃腫不堪
  • 易于理解、開發速度快、可維護性高

MVP

通過引入接口 BaseView,讓相應的視圖組件如 Activity,Fragment去實現 BaseView,把業務邏輯放在 presenter 層中,弱化 Model 只有跟 view 相關的操作都由 View 層去完成。

總結:

  • 徹底解決了 MVC 中 View 和 Controller 傻傻分不清楚的問題
  • 但是隨著業務邏輯的增加,一個頁面可能會非常復雜,UI 的改變是非常多,會有非常多的 case,這樣就會造成 View 的接口會很龐大
  • 更容易單元測試

MVVM

在 MVP 中 View 和 Presenter 要相互持有,方便調用對方,而在 MVP 中 View 和 ViewModel 通過 Binding 進行關聯,他們之前的關聯處理通過 DataBinding 完成。

總結:

  • 很好的解決了 MVC 和 MVP 的問題
  • 視圖狀態較多,ViewModel 的構建和維護的成本都會比較高
  • 但是由于數據和視圖的雙向綁定,導致出現問題時不太好定位來源

Jetpack

架構

使用示例

build.gradle

android {
    ···
    dataBinding {
        enabled = true
    }
}
dependencies {
    ···
    implementation "androidx.fragment:fragment-ktx:$rootProject.fragmentVersion"
    implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.lifecycleVersion"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$rootProject.lifecycleVersion"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.lifecycleVersion"
}
復制代碼

fragment_plant_detail.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="viewModel"
            type="com.google.samples.apps.sunflower.viewmodels.PlantDetailViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            ···
            android:text="@{viewModel.plant.name}"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {

    private val args: PlantDetailFragmentArgs by navArgs()
    private lateinit var shareText: String

    private val plantDetailViewModel: PlantDetailViewModel by viewModels {
        InjectorUtils.providePlantDetailViewModelFactory(requireActivity(), args.plantId)
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
                inflater, R.layout.fragment_plant_detail, container, false).apply {
            viewModel = plantDetailViewModel
            lifecycleOwner = this@PlantDetailFragment
        }

        plantDetailViewModel.plant.observe(this) { plant ->
            // 更新相關 UI
        }

        return binding.root
    }
}
復制代碼

Plant.kt

data class Plant (
    val name: String
)
復制代碼

PlantDetailViewModel.kt

class PlantDetailViewModel(
    plantRepository: PlantRepository,
    private val plantId: String
) : ViewModel() {

    val plant: LiveData<Plant>

    override fun onCleared() {
        super.onCleared()
        viewModelScope.cancel()
    }

    init {
        plant = plantRepository.getPlant(plantId)
    }
}
復制代碼

PlantDetailViewModelFactory.kt

class PlantDetailViewModelFactory(
    private val plantRepository: PlantRepository,
    private val plantId: String
) : ViewModelProvider.NewInstanceFactory() {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return PlantDetailViewModel(plantRepository, plantId) as T
    }
}
復制代碼

InjectorUtils.kt

object InjectorUtils {
    private fun getPlantRepository(context: Context): PlantRepository {
        ···
    }

    fun providePlantDetailViewModelFactory(
        context: Context,
        plantId: String
    ): PlantDetailViewModelFactory {
        return PlantDetailViewModelFactory(getPlantRepository(context), plantId)
    }
}
復制代碼

NDK 開發

NDK 全稱是 Native Development Kit,是一組可以讓你在 Android 應用中編寫實現 C/C++ 的工具,可以在項目用自己寫源代碼構建,也可以利用現有的預構建庫。

使用 NDK 的使用目的有:

  • 從設備獲取更好的性能以用于計算密集型應用,例如游戲或物理模擬
  • 重復使用自己或其他開發者的 C/C++ 庫,便利于跨平臺。
  • NDK 集成了譬如 OpenSL、Vulkan 等 API 規范的特定實現,以實現在 java 層無法做到的功能如提升音頻性能等
  • 增加反編譯難度

JNI 基礎

數據類型

  • 基本數據類型
Java 類型 Native 類型 符號屬性 字長
boolean jboolean 無符號 8位
byte jbyte 無符號 8位
char jchar 無符號 16位
short jshort 有符號 16位
int jnit 有符號 32位
long jlong 有符號 64位
float jfloat 有符號 32位
double jdouble 有符號 64位
  • 引用數據類型
Java 引用類型 Native 類型 Java 引用類型 Native 類型
All objects jobject char[] jcharArray
java.lang.Class jclass short[] jshortArray
java.lang.String jstring int[] jintArray
Object[] jobjectArray long[] jlongArray
boolean[] jbooleanArray float[] jfloatArray
byte[] jbyteArray double[] jdoubleArray
java.lang.Throwable jthrowable

String 字符串函數操作

JNI 函數 描述
GetStringChars / ReleaseStringChars 獲得或釋放一個指向 Unicode 編碼的字符串的指針(指 C/C++ 字符串)
GetStringUTFChars / ReleaseStringUTFChars 獲得或釋放一個指向 UTF-8 編碼的字符串的指針(指 C/C++ 字符串)
GetStringLength 返回 Unicode 編碼的字符串的長度
getStringUTFLength 返回 UTF-8 編碼的字符串的長度
NewString 將 Unicode 編碼的 C/C++ 字符串轉換為 Java 字符串
NewStringUTF 將 UTF-8 編碼的 C/C++ 字符串轉換為 Java 字符串
GetStringCritical / ReleaseStringCritical 獲得或釋放一個指向字符串內容的指針(指 Java 字符串)
GetStringRegion 獲取或者設置 Unicode 編碼的字符串的指定范圍的內容
GetStringUTFRegion 獲取或者設置 UTF-8 編碼的字符串的指定范圍的內容

常用 JNI 訪問 Java 對象方法

MyJob.java

package com.example.myjniproject;

public class MyJob {

    public static String JOB_STRING = "my_job";
    private int jobId;

    public MyJob(int jobId) {
        this.jobId = jobId;
    }

    public int getJobId() {
        return jobId;
    }
}
復制代碼

native-lib.cpp

#include <jni.h>

extern "C"
JNIEXPORT jint JNICALL
Java_com_example_myjniproject_MainActivity_getJobId(JNIEnv *env, jobject thiz, jobject job) {

    // 根據實力獲取 class 對象
    jclass jobClz = env->GetObjectClass(job);
    // 根據類名獲取 class 對象
    jclass jobClz = env->FindClass("com/example/myjniproject/MyJob");

    // 獲取屬性 id
    jfieldID fieldId = env->GetFieldID(jobClz, "jobId", "I");
    // 獲取靜態屬性 id
    jfieldID sFieldId = env->GetStaticFieldID(jobClz, "JOB_STRING", "Ljava/lang/String;");

    // 獲取方法 id
    jmethodID methodId = env->GetMethodID(jobClz, "getJobId", "()I");
    // 獲取構造方法 id
    jmethodID  initMethodId = env->GetMethodID(jobClz, "<init>", "(I)V");

    // 根據對象屬性 id 獲取該屬性值
    jint id = env->GetIntField(job, fieldId);
    // 根據對象方法 id 調用該方法
    jint id = env->CallIntMethod(job, methodId);

    // 創建新的對象
    jobject newJob = env->NewObject(jobClz, initMethodId, 10);

    return id;
}
復制代碼

NDK 開發

基礎開發流程

  • 在 java 中聲明 native 方法
public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.d("MainActivity", stringFromJNI());
    }

    private native String stringFromJNI();
}
復制代碼
  • app/src/main 目錄下新建 cpp 目錄,新建相關 cpp 文件,實現相關方法(AS 可用快捷鍵快速生成)

native-lib.cpp

#include <jni.h>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myjniproject_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
復制代碼
  • 函數名的格式遵循遵循如下規則:Java_包名類名方法名。
  • extern "C" 指定采用 C 語言的命名風格來編譯,否則由于 C 與 C++ 風格不同,導致鏈接時無法找到具體的函數
  • JNIEnv*:表示一個指向 JNI 環境的指針,可以通過他來訪問 JNI 提供的接口方法
  • jobject:表示 java 對象中的 this
  • JNIEXPORT 和 JNICALL:JNI 所定義的宏,可以在 jni.h 頭文件中查找到
  • 通過 CMake 或者 ndk-build 構建動態庫

System.loadLibrary()

java/lang/System.java:

@CallerSensitive
public static void load(String filename) {
    Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}
復制代碼
  • 調用 Runtime 相關 native 方法

java/lang/Runtime.java:

private static native String nativeLoad(String filename, ClassLoader loader, Class<?> caller);
復制代碼
  • native 方法的實現如下:

dalvik/vm/native/java_lang_Runtime.cpp:

static void Dalvik_java_lang_Runtime_nativeLoad(const u4* args,
    JValue* pResult)
{
    ···
    bool success;

    assert(fileNameObj != NULL);
    // 將 Java 的 library path String 轉換到 native 的 String
    fileName = dvmCreateCstrFromString(fileNameObj);

    success = dvmLoadNativeCode(fileName, classLoader, &reason);
    if (!success) {
        const char* msg = (reason != NULL) ? reason : "unknown failure";
        result = dvmCreateStringFromCstr(msg);
        dvmReleaseTrackedAlloc((Object*) result, NULL);
    }
    ···
}
復制代碼
  • dvmLoadNativeCode 函數實現如下:

dalvik/vm/Native.cpp

bool dvmLoadNativeCode(const char* pathName, Object* classLoader,
        char** detail)
{
    SharedLib* pEntry;
    void* handle;
    ···
    *detail = NULL;

    // 如果已經加載過了,則直接返回 true
    pEntry = findSharedLibEntry(pathName);
    if (pEntry != NULL) {
        if (pEntry->classLoader != classLoader) {
            ···
            return false;
        }
        ···
        if (!checkOnLoadResult(pEntry))
            return false;
        return true;
    }

    Thread* self = dvmThreadSelf();
    ThreadStatus oldStatus = dvmChangeStatus(self, THREAD_VMWAIT);
    // 把.so mmap 到進程空間,并把 func 等相關信息填充到 soinfo 中
    handle = dlopen(pathName, RTLD_LAZY);
    dvmChangeStatus(self, oldStatus);
    ···
    // 創建一個新的 entry
    SharedLib* pNewEntry;
    pNewEntry = (SharedLib*) calloc(1, sizeof(SharedLib));
    pNewEntry->pathName = strdup(pathName);
    pNewEntry->handle = handle;
    pNewEntry->classLoader = classLoader;
    dvmInitMutex(&pNewEntry->onLoadLock);
    pthread_cond_init(&pNewEntry->onLoadCond, NULL);
    pNewEntry->onLoadThreadId = self->threadId;

    // 嘗試添加到列表中
    SharedLib* pActualEntry = addSharedLibEntry(pNewEntry);

    if (pNewEntry != pActualEntry) {
        ···
        freeSharedLibEntry(pNewEntry);
        return checkOnLoadResult(pActualEntry);
    } else {
        ···
        bool result = true;
        void* vonLoad;
        int version;
        // 調用該 so 庫的 JNI_OnLoad 方法
        vonLoad = dlsym(handle, "JNI_OnLoad");
        if (vonLoad == NULL) {
            ···
        } else {
            // 調用 JNI_Onload 方法,重寫類加載器。
            OnLoadFunc func = (OnLoadFunc)vonLoad;
            Object* prevOverride = self->classLoaderOverride;

            self->classLoaderOverride = classLoader;
            oldStatus = dvmChangeStatus(self, THREAD_NATIVE);
            ···
            version = (*func)(gDvmJni.jniVm, NULL);
            dvmChangeStatus(self, oldStatus);
            self->classLoaderOverride = prevOverride;

            if (version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 &&
                version != JNI_VERSION_1_6)
            {
                ···
                result = false;
            } else {
                ···
            }
        }

        if (result)
            pNewEntry->onLoadResult = kOnLoadOkay;
        else
            pNewEntry->onLoadResult = kOnLoadFailed;

        pNewEntry->onLoadThreadId = 0;

        // 釋放鎖資源 
        dvmLockMutex(&pNewEntry->onLoadLock);
        pthread_cond_broadcast(&pNewEntry->onLoadCond);
        dvmUnlockMutex(&pNewEntry->onLoadLock);
        return result;
    }
}
復制代碼

CMake 構建 NDK 項目

CMake 是一個開源的跨平臺工具系列,旨在構建,測試和打包軟件,從 Android Studio 2.2 開始,Android Sudio 默認地使用 CMake 與 Gradle 搭配使用來構建原生庫。

啟動方式只需要在 app/build.gradle 中添加相關:

android {
    ···
    defaultConfig {
        ···
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }

        ndk {
            abiFilters 'arm64-v8a', 'armeabi-v7a'
        }
    }
    ···
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}
復制代碼

然后在對應目錄新建一個 CMakeLists.txt 文件:

# 定義了所需 CMake 的最低版本
cmake_minimum_required(VERSION 3.4.1)

# add_library() 命令用來添加庫
# native-lib 對應著生成的庫的名字
# SHARED 代表為分享庫
# src/main/cpp/native-lib.cpp 則是指明了源文件的路徑。
add_library( # Sets the name of the library.
        native-lib

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        src/main/cpp/native-lib.cpp)

# find_library 命令添加到 CMake 構建腳本中以定位 NDK 庫,并將其路徑存儲為一個變量。
# 可以使用此變量在構建腳本的其他部分引用 NDK 庫
find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log)

# 預構建的 NDK 庫已經存在于 Android 平臺上,因此,無需再構建或將其打包到 APK 中。
# 由于 NDK 庫已經是 CMake 搜索路徑的一部分,只需要向 CMake 提供希望使用的庫的名稱,并將其關聯到自己的原生庫中

# 要將預構建庫關聯到自己的原生庫
target_link_libraries( # Specifies the target library.
        native-lib

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})
···
復制代碼

常用的 Android NDK 原生 API

支持 NDK 的 API 級別 關鍵原生 API 包括
3 Java 原生接口 #include <jni.h>
3 Android 日志記錄 API #include <android/log.h>
5 OpenGL ES 2.0 #include <GLES2/gl2.h> #include <GLES2/gl2ext.h>
8 Android 位圖 API #include <android/bitmap.h>
9 OpenSL ES #include <SLES/OpenSLES.h> #include <SLES/OpenSLES_Platform.h> #include <SLES/OpenSLES_Android.h> #include <SLES/OpenSLES_AndroidConfiguration.h>
9 原生應用 API #include <android/rect.h> #include <android/window.h> #include<android/native_activity.h> ···
18 OpenGL ES 3.0 #include <GLES3/gl3.h> #include <GLES3/gl3ext.h>
21 原生媒體 API #include <media/NdkMediaCodec.h> #include <media/NdkMediaCrypto.h> ···
24 原生相機 API #include <camera/NdkCameraCaptureSession.h> #include <camera/NdkCameraDevice.h> ···
···

類加載器

雙親委托模式

某個特定的類加載器在接到加載類的請求時,首先將加載任務委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。

因為這樣可以避免重復加載,當父親已經加載了該類的時候,就沒有必要子 ClassLoader 再加載一次。如果不使用這種委托模式,那我們就可以隨時使用自定義的類來動態替代一些核心的類,存在非常大的安全隱患。

DexPathList

DexClassLoader 重載了 findClass 方法,在加載類時會調用其內部的 DexPathList 去加載。DexPathList 是在構造 DexClassLoader 時生成的,其內部包含了 DexFile。

DexPathList.java
public Class findClass(String name) {
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    return null;
}

作者:程序員喵大人
鏈接:https://juejin.cn/post/7021080876192628772

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容

  • 大綱 Java 基礎 1. ==、equals 和 hashCode 的區別 == 用于基礎數據類型的判斷時,比較...
    Parallel_Lines閱讀 890評論 0 2
  • RecyclerView與ListView 對比淺析:緩存機制 重要 一. 背景 PS:相關知識: ListVie...
    賢瑜閱讀 1,057評論 0 0
  • 面試必背 會舍棄、總結概括——根據我這些年面試和看面試題搜集過來的知識點匯總而來 建議根據我的寫的面試應對思路中的...
    luoyangzk閱讀 6,789評論 6 173
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月,有人笑有人哭,有人歡樂有人憂愁,有人驚喜有人失落,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,578評論 28 53
  • 首先介紹下自己的背景: 我11年左右入市到現在,也差不多有4年時間,看過一些關于股票投資的書籍,對于巴菲特等股神的...
    瞎投資閱讀 5,755評論 3 8