天天看點

插件化的基石 -- apk動态加載

随着我街業務的蓬勃發展,産品和營運随時上新功能新活動的需求越來越強烈,經常可以聽到“有個功能我想周x上,行不行”。行麼?當然是不行啦,上新功能得發新版本啊,到時候費時費力打亂開發節奏不說,覆寫率也是個問題。蘇格拉底曾經說過:“現在移動端的主要沖突是産品日益增長的功能需求與平台落後的釋出流程之間的沖突”。

當然,作為一個靠譜的程式猿,我們就是為了滿足産品的需求而存在的(正義臉)。于是在一個陽光明媚的早晨,吃完公司的免費早餐後,我和小強、葉開,決定做一個完善的Android動态加載架構。

Android動态加載技術在蘑菇街的第一次實踐,還是在14年的時候,使用的就是之前網上廣(tu)為(du)流(si)傳(fang)的方式,這種方式有一個重大缺陷,就是插件内部對資源的通路隻能通過自己定義的方式,包括對layout檔案的inflate等,使用getResouces的方式,分分鐘crash給你看,而且内部實作有些複雜,容易出現莫名其妙的ResourcesNotFound錯誤。在一段時間的使用之後,始終無法大面積推廣,原因就是對開發人員來說,寫一個“正常”的子產品和寫一個動态加載子產品,寫法是不一樣的。這件事一直如哏在喉,如果這個架構無法做到對開發業務的同學們透明,那麼就很難推廣開去。如何做到對業務開發者透明呢,最重要的是對于各類系統api的使用,尤其是Android四大元件的使用和資源通路,都要遵循系統提供的方式。

抛開上面的東西,從頭開始講述一下動态加載的原理:

Android應用程式的.java檔案在編譯期會通過javac指令編譯成.class檔案,最後再把所有的.class檔案編譯成.dex檔案放在.apk包裡面。那麼動态加載就是在運作時把插件apk直接加載到classloader裡面的技術。

看完上面的原理,不知道你有沒有什麼疑問,反正我是有的。

  1. 如何加載插件裡面的.dex檔案。
  2. apk裡面的資源怎麼辦。

上面兩個問題是動态加載架構最重要的兩點,無法動态安裝dex或資源檔案的動态加載架構都是耍流氓。我們在實作這個架構的時候同樣也遇到了這兩個問題。

如何動态加載插件代碼:

關于代碼加載,系統提供了DexClassLoader來加載插件代碼。開發者可以對每一個插件配置設定一個DexClassLoader(這是目前最常見的一種方式),也可以動态得把插件加載到目前運作環境的classloader中。蘑菇街采用的是後者,這種方式可以有效的防止各種莫名其妙的ClassCastException,當你在crash背景看到各種 A cast A錯誤而欲哭無淚的時候,我想你會喜歡上這種方式。

事情當然不會這麼簡單,系統提供的DexClassloader對外api中,隻有一種方式可以向類加載器指定加載路徑。就是在構造函數中傳入apk/zip/dex路徑。這完全不符合我們“動态”的原則,難道每次加載一個插件,都必須重新執行個體化一個類加載器出來嗎?這個時候我們想到了google提供的multidex插件,這個插件旨在幫助函數超過65536上限的應用在編譯期切割class到多個dex檔案中。經過觀察發現,5.0以下的Android系統,在應用安裝的時候隻認classes.dex檔案,并在安裝期對這個dex檔案進行opt操作,生成的odex檔案放在/data/dalivk-cache裡面。那麼剩下classes(N).dex怎麼辦呢,答案就是如果在編譯期使用multidex插件的話,開發者還需要讓自己的Application繼承MultiDexApplication,這樣說起來,這個MultiDexApplication應該就有加載剩下的classes(N).dex的能力了。檢視MultiDexApplication代碼,果然找到了線索:

public class MultiDexApplication extends Application {
  @Override
  protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);
  }
}
           

可以看到,它在attachBaseContext函數調用了support包中MultiDex類的install函數來安裝classes(N).dex,于是都是應用層代碼,它能動态安裝那表示我們也可以。有了以上的分析,剩下要做的就隻是去扒一扒install這個函數了。

如何動态加載插件資源:

我們在開發的時候,當有需要用到資源的地方,可以直接調用Context的getResources()函數傳回Resources的來通路打包在apk中的資源檔案。在研究如何動态添加資源到系統的Resources對象的時候,有必要先了解一下Resources本身是如何通路到資源的。

檢視系統的Resources源碼,我們發現這個類主要做了兩件事,首當其沖的當然是通路資源,另外一件就是管理資源配置資訊。對于資源的動态加載來說,我們關心的是它如何做第一件事的。實際上,Resources對資源的通路,全部代理給了另一個重要的對象AssetManager。那麼問題轉化成了,AssetManager是如何做到對資源的通路的。Resources類在它的構造函數裡對AssetManager做了一些重要的初始化:

public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config,
            CompatibilityInfo compatInfo, IBinder token) {
        mAssets = assets;
        mMetrics.setToDefaults();
        if (compatInfo != null) {
            mCompatibilityInfo = compatInfo;
        }
        mToken = new WeakReference<IBinder>(token);
        updateConfiguration(config, metrics);
        assets.ensureStringBlocks();
}
           

其中的重點就是調用了AssetManager對象的ensureStringBlocks()函數,這個函數的實作如下:

/*package*/ final void ensureStringBlocks() {
    if (mStringBlocks == null) {
        synchronized (this) {
            if (mStringBlocks == null) {
                makeStringBlocks(sSystem.mStringBlocks);
            }
        }
    }
}
           

函數先判斷mStringBlocks變量是否為空,如果不為空的話,表示需要被初始化,于是調用makeStringBlocks函數初始化mStringBlocks:

/*package*/ final void makeStringBlocks(StringBlock[] seed) {
    final int seedNum = (seed != null) ? seed.length : 0;
    final int num = getStringBlockCount();
    mStringBlocks = new StringBlock[num];
    if (localLOGV) Log.v(TAG, "Making string blocks for " + this
            + ": " + num);
    for (int i=0; i<num; i++) {
        if (i < seedNum) {
            mStringBlocks[i] = seed[i];
        } else {
            mStringBlocks[i] = new StringBlock(getNativeStringBlock(i), true);
        }
    }
}
           

這裡的mStringBlocks對象是一個StringBlock數組,這個類被标記為@hide,表示應用層根本不需要關心它的存在。那麼它是做什麼用的呢,它就是AssetManager能夠通路資源的奧秘所在,AssetManager所有通路資源的函數,例如getResourceTextArray(),都最終通過StringBlock再代理到native進行通路的。看到這裡,依然沒有任何看到能夠訓示為什麼開發者可以通路自己應用的資源,那麼我們再看得前面一點,看看傳入Resources的構造函數之前,asset參數是不是被“做過手腳”。函數調用輾轉到ResourceManager的getTopLevelResources函數:

public Resources getTopLevelResources(String resDir, String[] splitResDirs,
                String[] overlayDirs, String[] libDirs, int displayId,
                Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
    ...
    AssetManager assets = new AssetManager();
    if (resDir != null) {
        if (assets.addAssetPath(resDir) == 0) {
              return null;
        }
    }
    ...
}
           

函數代碼有點多,截取最重要的部分,那就是系統通過調用AssetManager的addAssetPath函數,将需要加載的資源路徑加了進去。addAssetPath函數傳回一個int類型,它訓示了每個被添加的資源路徑在native層一個數組中的位置,這個數組儲存了系統資源路徑(framework-res.apk),和應用自己添加的所有的資源路徑。再回過頭看makeStringBlocks函數,就豁然開朗了:

  1. makeStringBlocks函數的參數也是一個StringBlock數組,它表示系統資源,首先它調用getStringBlockCount函數得到目前應用所有要加載的資源路徑數量。
  2. 然後進入循環,如果屬于系統資源,就直接用傳入參數seed中的對象來指派。
  3. 如果是應用自己的資源,就執行個體化一個新的StringBlock對象來指派。并在StringBlock的構造函數中調用getNativeStringBlock函數來擷取一個native層的對象指針,這個指針被java層StringBlock對象用來調用native函數,最終達到通路資源的目的。

有興趣的同學可以繼續深入native層的源碼,可以看到不管是addAssetPath函數還是makeStringBlocks函數,使用的都是native層同一個數組,這樣,這兩個函數就被關聯了起來。

到這裡,我們已經知道了如何動态添加資源路徑的“秘密”。

解決了以上兩個問題,一個基本滿足要求的動态加載架構就被搭了起來。

上面已經交代了如何把一個插件apk動态加載到記憶體中,并做到可用。下面介紹的是,我們如何在“合适”的時間加載某個插件。

試想一個理想化的場景,一個app中,所有的子產品都是一個個的插件,我們不可能在應用初始化的時候把所有的插件全部加載,dex optimize時長都能把人弄哭。

那麼就得找一個時間點,使我們可以知道,某個插件第一次被用到是什麼時候,隻要在這個時間加載這個插件就好。

分析一下插件主要有哪些東西構成:

  1. 首先是邏輯代碼,這些函數有些是自己插件内部用的,有些是外部其他插件也要用的。
  2. 頁面,各種Activity。

Ok,我們一個個得解決上面的問題

對外函數

在這種情況下,如果一個插件A需要調用插件B的函數,我們可以去檢查插件B是否被加載過,如果還未加載,就加載它。

這裡不知道大家會不會有個疑問,我們是如何感覺到插件A調用了插件B中的函數了呢?

首先有個天然性的隔離就是,插件與插件之間在開發的時候,都是獨立工程的,它們之間不應該存在直接的函數調用。

然後,我們需要一個中間管理架構,用來統一處理插件間的函數(服務)調用,這也是蘑菇街元件管理架構所做的很重要的一件事。所有的插件/元件之間的函數(服務)調用,必須過管理架構中轉,最後由架構進行實際的調用。

由于管理架構是一個通用架構,并不針對某個插件,是以插件所能提供的對外函數,要使用配置的方式告知外部和管理架構,未在配置定義的函數是不允許被調用的。

于是我們可以攔截到插件間的函數(服務)調用了,這是第一個可以檢測到某個插件是否開始被使用的地方。

頁面,Activity

另外一種常見情況就是插件A需要啟動插件B中的一個Activity。為了讓開發插件的同學最小化得感覺自己在開發一個插件,業務開發的同學依然可以使用任何系統提供的任何啟動Activity的方式。但是作為需要知道啟動了哪個Activity以便去安裝某個插件的管理架構來說,需要對這個操作進行攔截。

又到了read the fu*k source code的時間啦~

首先我們從一個常見的startActivity函數調用開始,不論調用了哪個Context的startActivity函數,最後都會調用到ContextImpl的startActivity函數:

@Override
public void startActivity(Intent intent, Bundle options) {
    ...
    mMainThread.getInstrumentation().execStartActivity(
        getOuterContext(), mMainThread.getApplicationThread(), null,
        (Activity)null, intent, -1, options);
}
           

看到它最後調用了mMainThread.getInstrumentation().execStartActivity來啟動一個Activity,mMainThread是一個ActivityThread類型的對象,getInstrumentation函數傳回一個Instrumentation類型的對象。順理成章得,接下去是Instrumentation類的execStartActivity函數:

public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
    ...
    try {
        ...
        int result = ActivityManagerNative.getDefault()
            .startActivity(whoThread, who.getBasePackageName(), intent,
                    intent.resolveTypeIfNeeded(who.getContentResolver()),
                    token, target != null ? target.mEmbeddedID : null,
                    requestCode, 0, null, options);
        checkStartActivityResult(result, intent);
    } catch (RemoteException e) {
    }
    return null;
}
           

調用到了ActivityManagerNative的startActivity函數,這是一個IPC調用,用于向遠端的ActivityManagerService發起一個start activity的請求。當然大家可以攔截Instrumentation的execStartActivity函數,因為通過函數的入參,已經知道了應用層請求啟動哪個Activity了。但攔截這個函數有幾個弊端,

  1. 第一就是execStartActivity函數隻是啟動Activity的某一個函數,類似的函數還有一些,如果我們都把它們攔截了,顯得有點備援了。
  2. 第二個弊端就是這裡隻是向ActivityManagerService發起一個IPC請求,具體這個Activity能否被啟動,都還是未知數,如果在這裡攔截而這個Activity,也會造成一些浪費和不确定性。

另外我們可以非常肯定得認為,在整個Activity啟動的過程中,執行個體化某個具體Activity的操作一定是在目前應用程式程序進行的,不可能由遠端的ActivityManagerService來執行,因為隻有目前應用程式進行的記憶體中,才會有某個具體Activity的類存在。

于是我們在ActivityThread類中找到了最後執行個體化某個Activity類的函數

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

    ...

    Activity activity = null;
    try {
        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        r.intent.prepareToEnterProcess();
        if (r.state != null) {
            r.state.setClassLoader(cl);
        }
    } catch (Exception e) {
        if (!mInstrumentation.onException(activity, e)) {
            throw new RuntimeException(
                "Unable to instantiate activity " + component
                + ": " + e.toString(), e);
        }
    }

    ...

    return activity;
}
           

最關鍵的一句,就是調用mInstrumentation.newActivity來傳回一個Activity對象,是以這裡是最後執行個體化的地方,Instrumentation類的newActivity也就是我們需要攔截下來的操作。簡單明了,隻需要複寫這個函數然後把自己的Instrumentation對象替換成運作上下文的就可以了~

到這裡為止,Activity啟動也被管理架構感覺到了,自然可以去檢測安裝Activity所屬的插件了。

以上就是蘑菇街元件與插件管理架構中插件安裝的邏輯。

繼續閱讀