Android 面試之必問高級知識點

Android 面試之必問Java基礎
Android 面試之必問Android基礎知識javascript

1,編譯模式

1.1 概念

在Android早期的版本中,應用程序的運行環境是須要依賴Dalvik虛擬機的。不過,在後來的版本(大概是4.x版本),Android的運行環境卻換到了 Android Runtime,其處理應用程序執行的方式徹底不一樣於 Dalvik,Dalvik 是依靠一個 Just-In-Time (JIT) 編譯器去解釋字節碼。前端

不過,Dalvik模式下,開發者編譯後的應用代碼須要經過一個解釋器在用戶的設備上運行,這一機制並不高效,但讓應用能更容易在不一樣硬件和架構上運 行。ART 則徹底改變了這套作法,在應用安裝時就預編譯字節碼到機器語言,這一機制叫 Ahead-Of-Time (AOT)編譯。在移除解釋代碼這一過程後,應用程序執行效率更高、啓動也更快。java

1.2 AOT優勢

下面是AOT編譯方式的一些優勢:android

1.2.1 預先編譯

ART 引入了預先編譯機制,可提升應用的性能。ART 還具備比 Dalvik 更嚴格的安裝時驗證。在安裝時,ART 使用設備自帶的 dex2oat 工具來編譯應用。該實用工具接受 DEX 文件做爲輸入,併爲目標設備生成通過編譯的應用可執行文件,該工具可以順利編譯全部有效的 DEX 文件。git

1.2.2 垃圾回收優化

垃圾回收 (GC) 可能有損於應用性能,從而致使顯示不穩定、界面響應速度緩慢以及其餘問題。ART模式從如下幾個方面優化了垃圾回收的策略:github

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

1.2.3 開發和調試方面的優化

支持採樣分析器
一直以來,開發者都使用 Traceview 工具(用於跟蹤應用執行狀況)做爲分析器。雖然 Traceview 可提供有用的信息,但每次方法調用產生的開銷會致使 Dalvik 分析結果出現誤差,並且使用該工具明顯會影響運行時性能ART 添加了對沒有這些限制的專用採樣分析器的支持,於是可更準確地瞭解應用執行狀況,而不會明顯減慢速度。支持的版本從KitKat (4.4)版本開始,爲 Dalvik 的 Traceview 添加了採樣支持。web

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

優化了異常和崩潰報告中的診斷詳細信息
當發生運行時異常時,ART 會爲您提供儘量多的上下文和詳細信息。ART 會提供 java.lang.ClassCastException、java.lang.ClassNotFoundException 和 java.lang.NullPointerException 的更多異常詳細信息(較高版本的 Dalvik 會提供 java.lang.ArrayIndexOutOfBoundsException 和 java.lang.ArrayStoreException 的更多異常詳細信息,這些信息如今包括數組大小和越界偏移量;ART 也提供這類信息)。算法

1.3 垃圾回收

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

除了新的垃圾回收器以外,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 會進行一些操做(例如,壓縮和監視器壓縮),從而致使應用線程長時間暫停。

目前,Android的ART正在使用的兩個移動 GC 是同構空間壓縮和半空間壓縮,它們的區別以下:

  • 半空間壓縮:將對象在兩個緊密排列的碰撞指針空間之間進行移動。這種移動 GC 適用於小內存設備,由於它能夠比同構空間壓縮稍微多節省一點內存,額外節省出的空間主要來自緊密排列的對象,這樣能夠避免 RosAlloc/DlMalloc 分配器佔用開銷。
  • 同構空間壓縮經過將對象從一個 RosAlloc 空間複製到另外一個 RosAlloc 空間來實現。這有助於經過減小堆碎片來減小內存使用量。這是目前非低內存設備的默認壓縮模式。相比半空間壓縮,同構空間壓縮的主要優點在於應用從後臺切換到前臺時無需進行堆轉換。

2,類加載器

2.1 類加載器分類

目前,Android的類加載器從下到上主要分爲BootstrapClassLoader(根類加載器)、 ExtensionClassLoader (擴展類加載器)和 AppClassLoader(應用類加載器)三種。

  • 根類加載器:該加載器沒有父加載器。它負責加載虛擬機的核心類庫,如java.lang.*等。例如java.lang.Object就是由根類加載器加載的。根類加載器從系統屬性sun.boot.class.path所指定的目錄中加載類庫。根類加載器的實現依賴於底層操做系統,屬於虛擬機的實現的一部分,它並無繼承java.lang.ClassLoader類。
  • 擴展類加載器:它的父加載器爲根類加載器。它從java.ext.dirs系統屬性所指定的目錄中加載類庫,或者從JDK的安裝目錄的jre/lib/ext子目錄(擴展目錄)下加載類庫,若是把用戶建立的JAR文件放在這個目錄下,也會自動由擴展類加載器加載。擴展類加載器是純Java類,是java.lang.ClassLoader類的子類。
  • 系統類加載器:也稱爲應用類加載器,它的父加載器爲擴展類加載器。它從環境變量classpath或者系統屬性java.class.path所指定的目錄中加載類,它是用戶自定義的類加載器的默認父加載器。系統類加載器是純Java類,是java.lang.ClassLoader類的子類。
    父子加載器並不是繼承關係,也就是說子加載器不必定是繼承了父加載器。

2.2 雙親委託模式

所謂雙親委託模式,指的是某個特定的類加載器在接到加載類的請求時,首先將加載任務委託給父類加載器,依次遞歸,若是父類加載器能夠完成類加載任務,就成功返回;只有父類加載器沒法完成此加載任務時,才本身去加載。

由於這樣能夠避免重複加載,當父親已經加載了該類的時候,就沒有必要子 ClassLoader 再加載一次。若是不使用這種委託模式,那咱們就能夠隨時使用自定義的類來動態替代一些核心的類,存在很是大的安全隱患。

舉個例子,事實上,java.lang.String這個類並不會被咱們自定義的classloader加載,而是由bootstrap classloader進行加載,爲何會這樣?實際上這就是雙親委託模式的緣由,由於在任何一個自定義ClassLoader加載一個類以前,它都會先 委託它的父親ClassLoader進行加載,只有當父親ClassLoader沒法加載成功後,纔會由本身加載。

2.3 Android的類加載器

下面是Android類加載器的模型圖:
在這裏插入圖片描述
下面看一下DexClassLoader,DexClassLoader 重載了 findClass 方法,在加載類時會調用其內部的 DexPathList 去加載。DexPathList 是在構造 DexClassLoader 時生成的,其內部包含了 DexFile,涉及的源碼以下。

···
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;
}
···

類加載器更多的內容,能夠參考:android 類加載器雙親委託模式

3,Android Hook

所謂Hook,就是在程序執行的過程當中去截取其中的某段信息,示意圖以下。
說到

Android的Hook大致的流程能夠分爲以下幾步:
一、根據需求肯定須要 hook 的對象
二、尋找要hook的對象的持有者,拿到須要 hook 的對象
三、定義「要 hook 的對象」的代理類,而且建立該類的對象
四、使用上一步建立出來的對象,替換掉要 hook 的對象

下面是一段簡單的Hook的示例代碼,用到了Java的反射機制。

@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);
        }
    }
}

而在Android開發中,想要實現Hook,確定是沒有這麼簡單的,咱們須要藉助一些Hook框架,好比Xposed、Cydia Substrate、Legend等。

參考資料:Android Hook機制

4,代碼混淆

4.1 Proguard

衆所周知,Java代碼是很是容易反編譯的,爲了更好的保護Java源代碼,咱們每每會對編譯好的Class類文件進行混淆處理。而ProGuard就是一個混淆代碼的開源項目。它的主要做用就是混淆,固然它還能對字節碼進行縮減體積、優化等,但那些對於咱們來講都算是次要的功能。

具體來講,ProGuard具備以下功能:

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

在Android開發中,開啓混淆須要將app/build.gradle文件下的minifyEnabled屬性設置爲true,以下所示。

minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

proguard-android.txt是Android提供的默認混淆配置文件,咱們須要的混淆的規則都放在這個文件中。

4.2 混淆規則

混淆命令

  • keep:保留類和類中的成員,防止被混淆或移除
  • keepnames:保留類和類中的成員,防止被混淆,成員沒有被引用會被移除
  • keepclassmembers:只保留類中的成員,防止被混淆或移除
  • keepclassmembernames:只保留類中的成員,防止被混淆,成員沒有引用會被移除
  • keepclasseswithmembers:保留類和類中的成員,防止被混淆或移除,保留指明的成員
  • keepclasseswithmembernames:保留類和類中的成員,防止被混淆,保留指明的成員,成員沒有引用會被移除

混淆通配符

  • <field>:匹配類中的全部字段
  • <method>:匹配類中全部的方法
  • <init>:匹配類中全部的構造函數
  • *: 匹配任意長度字符,不包含包名分隔符(.)
  • **: 匹配任意長度字符,包含包名分隔符(.)
  • ***: 匹配任意參數類型

keep的規則的格式以下:

[keep命令] [類] {
        [成員]
}

4.3 混淆模版

ProGuard中有些公共的模版是能夠複用的,好比壓縮比、大小寫混合和一些系統提供的Activity、Service不能混淆等。

# 代碼混淆壓縮比,在 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 *;
}

若是是aar這種插件,能夠在aar的build.gralde中添加以下混淆配置。

android {
    ···
    defaultConfig {
        ···
        consumerProguardFile 'proguard-rules.pro'
    }
    ···
}

5,NDK

若是要問Android的高級開發知識,那麼NDK確定是必問的。那麼什麼的NDK,NDK 全稱是 Native Development Kit,是一組可讓開發者在 Android 應用中使用C/C++ 的工具。一般,NDK能夠用在以下的場景中:

  • 從設備獲取更好的性能以用於計算密集型應用,例如遊戲或物理模擬。
  • 重複使用本身或其餘開發者的 C/C++ 庫,便利於跨平臺。
  • NDK 集成了譬如 OpenSL、Vulkan 等 API 規範的特定實現,以實如今 Java 層沒法作到的功能,如音視頻開發、渲染。
  • 增長反編譯難度。

5.1, JNI基礎

JNI即java native interface,是Java和Native代碼進行交互的接口。

5.1.1 JNI 訪問 Java 對象方法

假如,有以下一個Java類,代碼以下。

package com.xzh.jni;

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;
    }
}

而後,在cpp目錄下,新建native_lib.cpp,添加對應的native實現。

#include <jni.h>

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

    // 根據實例獲取 class 對象
    jclass jobClz = env->GetObjectClass(job);
    // 根據類名獲取 class 對象
    jclass jobClz = env->FindClass("com/xzh/jni/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;
}

5.2 NDK開發

5.2.1 基本流程

首先,在 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();
}

而後,新建一個 cpp 目錄,而且新建一個名爲native-lib.cpp的cpp 文件,實現相關方法。

#include <jni.h>

extern "C" JNIEXPORT jstring JNICALL
Java_com_xzh_jni_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

cpp文件遵循以下的規則:

  • 函數名的格式遵循遵循以下規則:Java_包名_類名_方法名。
  • extern "C" 指定採用 C 語言的命名風格來編譯,不然因爲 C 與 C++ 風格不一樣,致使連接時沒法找到具體的函數
  • JNIEnv*:表示一個指向 JNI 環境的指針,能夠經過他來訪問 JNI 提供的接口方法
  • jobject:表示 java 對象中的 this
  • JNIEXPORT 和 JNICALL:JNI 所定義的宏,能夠在 jni.h 頭文件中查找到

System.loadLibrary()的代碼位於java/lang/System.java文件中,源碼以下:

@CallerSensitive
public static void load(String filename) {
    Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}

5.3 CMake 構建 NDK

CMake 是一個開源的跨平臺工具系列,旨在構建、測試和打包軟件,從 Android Studio 2.2 開始,Android Sudio 默認地使用 CMake 與 Gradle 搭配使用來構建原生庫。具體來講,咱們可使用 Gradle 將 C \ C++ 代碼 編譯到原生庫中,而後將這些代碼打包到咱們的應用中, Java 代碼隨後能夠經過 Java 原生接口 ( JNI ) 調用 咱們原生庫中的函數。

使用CMake開發NDK項目須要下載以下一些套件:

  • Android 原生開發工具包 (NDK):這套工具集容許咱們 開發 Android 使用 C 和 C++ 代碼,並提供衆多平臺庫,讓咱們能夠管理原生 Activity 和訪問物理設備組件,例如傳感器和觸摸輸入。
  • CMake:一款外部構建工具,可與 Gradle 搭配使用來構建原生庫。若是你只計劃使用 ndk-build,則不須要此組件。
  • LLDB:一種調試程序,Android Studio 使用它來調試原生代碼。

咱們能夠打開Android Studio,依次選擇 【Tools】 > 【Android】> 【SDK Manager】> 【SDK Tools】選中LLDB、CMake 和 NDK便可。

啓用CMake還須要在 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開發基礎

6,動態加載

6.1 基本概念

動態加載技術在Web中很常見,對於Android項目來講,動態加載的目的是讓用戶不用從新安裝APK就能升級應用的功能,主要的應用場景是插件化和熱修復。

首先須要明確的一點,插件化和熱修復不是同一個概念,雖然站在技術實現的角度來講,他們都是從系統加載器的角度出發,不管是採用hook方式,亦或是代理方式或者是其餘底層實現,都是經過「欺騙」Android 系統的方式來讓宿主正常的加載和運行插件(補丁)中的內容;可是兩者的出發點是不一樣的。

插件化,本質上是把須要實現的模塊或功能當作一個獨立的功能提取出來,減小宿主的規模,當須要使用到相應的功能時再去加載相應的模塊。而熱修復則每每是從修復bug的角度出發,強調的是在不須要二次安裝應用的前提下修復已知的bug。

爲了方便說明,咱們先理清幾個概念:

  • 宿主: 當前運行的APP。
  • 插件: 相對於插件化技術來講,就是要加載運行的apk類文件。
  • 補丁: 相對於熱修復技術來講,就是要加載運行的.patch,.dex,*.apk等一系列包含dex修復內容的文件。

下圖展現了Android動態化開發框架的總體的架構。
在這裏插入圖片描述

6.2 插件化

關於插件化技術,最先能夠追溯到2012年的 AndroidDynamicLoader ,其原理是動態加載不一樣的Fragment實現UI替換,不過隨着15,16年更好的方案,這個方案漸漸的被淘汰了。再後來有了任玉剛的dynamic-load-apk方案,開始有了插件化的標準方案。然後面的方案大多基於Hook和動態代理兩個方向進行。

目前,插件化的開發並無一個官方的插件化方案,它是國內提出的一種技術實現,利用虛擬機的類的加載機制實現的一種技術手段,每每須要hook一些系統api,而Google從Android9.0開始限制對系統私有api的使用,也就形成了插件化的兼容性問題,如今幾個流行的插件化技術框架,都是大廠根據本身的需求,開源出來的,如滴滴的VirtualAPK,360的RePlugin等,你們能夠根據須要自行了解技術的實現原理。

6.3 熱修復

6.3.1 熱修復原理

說到熱修復的原理,就不得不提到類的加載機制,和常規的JVM相似,在Android中類的加載也是經過ClassLoader來完成,具體來講就是PathClassLoader 和 DexClassLoader 這兩個Android專用的類加載器,這兩個類的區別以下。

  • PathClassLoader:只能加載已經安裝到Android系統中的apk文件(/data/app目錄),是Android默認使用的類加載器。
  • DexClassLoader:能夠加載任意目錄下的dex/jar/apk/zip文件,也就是咱們一開始提到的補丁。

這兩個類都是繼承自BaseDexClassLoader,BaseDexClassLoader的構造函數以下。

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

這個構造函數只作了一件事,就是經過傳遞進來的相關參數,初始化了一個DexPathList對象。DexPathList的構造函數,就是將參數中傳遞進來的程序文件(就是補丁文件)封裝成Element對象,並將這些對象添加到一個Element的數組集合dexElements中去。

前面說過類加載器的做用,就是將一個具體的類(class)加載到內存中,而這些操做是由虛擬機完成的,對於開發者來講,只須要關注如何去找到這個須要加載的類便可,這也是熱修復須要乾的事情。

在Android中,查找一個名爲name的class須要經歷以下兩步:

  1. 在DexClassLoader的findClass 方法中經過一個DexPathList對象findClass()方法來獲取class。
  2. 在DexPathList的findClass 方法中,對以前構造好dexElements數組集合進行遍歷,一旦找到類名與name相同的類時,就直接返回這個class,找不到則返回null。

所以,基於上面的理論,咱們能夠想到一個最簡單的熱修復方案。假設如今代碼中的某一個類出現Bug,那麼咱們能夠在修復Bug以後,將這些個類打包成一個補丁文件,而後經過這個補丁文件封裝出一個Element對象,而且將這個Element對象插到原有dexElements數組的最前端。這樣,當DexClassLoader去加載類時,因爲雙親加載機制的特色,就會優先加載插入的這個Element,而有缺陷的Element則沒有機會再被加載。事實上,QQ早期的熱修復方案就是這樣的。

6.3.2 QQ 空間超級補丁方案

QQ 空間補丁方案就是使用javaassist 插樁的方式解決了CLASS_ISPREVERIFIED的難題。涉及的步驟以下:

  • 在apk安裝的時候系統會將dex文件優化成odex文件,在優化的過程當中會涉及一個預校驗的過程。
  • 若是一個類的static方法,private方法,override方法以及構造函數中引用了其餘類,並且這些類都屬於同一個dex文件,此時該類就會被打上CLASS_ISPREVERIFIED。

    • 若是在運行時被打上CLASS_ISPREVERIFIED的類引用了其餘dex的類,就會報錯。
  • 正常的分包方案會保證相關類被打入同一個dex文件。
  • 想要使得patch能夠被正常加載,就必須保證類不會被打上CLASS_ISPREVERIFIED標記。而要實現這個目的就必需要在分完包後的class中植入對其餘dex文件中類的引用。

6.3.3 Tinker

QQ空間超級補丁方案在遇到補丁文件很大的時候耗時是很是嚴重的,由於一個大文件夾加載到內存中構建一個Element對象時,插入到數組最前端是須要耗費時間的,而這很是影響應用的啓動速度。基於這些問題,微信提出了Tinker 方案。

Tinker的思路是,經過修復好的class.dex 和原有的class.dex比較差生差量包補丁文件patch.dex,在手機上這個patch.dex又會和原有的class.dex 合併生成新的文件fix_class.dex,用這個新的fix_class.dex 總體替換原有的dexPathList的中的內容,進而從根本上修復Bug,下圖是演示圖。

在這裏插入圖片描述
相比QQ空間超級補丁方案,Tinker 提供的思路能夠說效率更高。對Tinker熱修復方案感興趣的同窗能夠去看看Tinker 源碼分析之DexDiff / DexPatch

6.3.4 HotFix

以上提到的兩種方式,雖然策略有所不一樣,但總的來講都是從上層ClassLoader的角度出發,因爲ClassLoader的特色,若是想要新的補丁文件再次生效,不管你是插樁仍是提早合併,都須要從新啓動應用來加載新的DexPathList,從而實現Bug的修復。

AndFix 提供了一種運行時在Native修改Filed指針的方式,實現方法的替換,達到即時生效無需重啓,對應用無性能消耗的目的。不過,因爲Android在國內變成了安卓,各大手機廠商定製了本身的ROM,因此不少底層實現的差別,致使AndFix的兼容性並非很好。

6.3.5 Sophix

Sophix採用的是相似類修復反射注入方式,把補丁so庫的路徑插入到nativeLibraryDirectories數組的最前面, 這樣加載so庫的時候就是補丁so庫而不是原來的so庫。

在修復類代碼的缺陷時,Sophix對舊包與補丁包中classes.dex的順序進行了打破與重組,使得系統能夠天然地識別到這個順序,以實現類覆蓋的目的。

在修復資源的缺陷時,Sophix構造了一個package id 爲 0x66 的資源包,這個包裏只包含改變了的資源項,而後直接在原有AssetManager中addAssetPath這個包便可,無需變動AssetManager對象的引用。

除了這些方案外,熱修復方案還有美團的Robust、餓了嗎的Amigo等。不過,對於Android的熱修復來講,很難有一種十分完美的解決方案。好比,在Android開發中,四大組件使用前須要在AndroidManifest中提早聲明,而若是須要使用熱修復的方式,不管是提早佔坑亦或是動態修改,都會帶來很強的侵入性。同時,Android碎片化的問題,對熱修復方案的適配也是一大考驗。

參考:Android 熱修復的簡析
深刻探索Android熱修復技術原理

相關文章
相關標籤/搜索