Android動態加載進階 代理Activity模式

基本信息

技術背景

簡單模式中,使用ClassLoader加載外部的Dex或Apk文件,能夠加載一些本地APP不存在的類,從而執行一些新的代碼邏輯。可是使用這種方法卻不能直接啓動插件裏的Activity。git

啓動沒有註冊的Activity的兩個主要問題

Activity等組件是須要在Manifest中註冊後才能以標準Intent的方式啓動的(若是有興趣強烈推薦你瞭解下Activity生命週期實現的機制及源碼),經過ClassLoader加載並實例化的Activity實例只是一個普通的Java對象,能調用對象的方法,可是它沒有生命週期,並且Activity等系統組件是須要Android的上下文環境的(Context等資源),沒有這些東西Activity根本沒法工做。github

使用插件APK裏的Activity須要解決兩個問題segmentfault

  1. 如何使插件APK裏的Activity具備生命週期;安全

  2. 如何使插件APK裏的Activity具備上下文環境(使用R資源);服務器

代理Activity模式爲解決這兩個問題提供了一種思路。cookie

代理Activity模式

這種模式也是咱們項目中,繼「簡單動態加載模式」以後,第二種投入實際生產項目的開發方式。app

其主要特色是:主項目APK註冊一個代理Activity(命名爲ProxyActivity),ProxyActivity是一個普通的Activity,但只是一個空殼,自身並無什麼業務邏輯。每次打開插件APK裏的某一個Activity的時候,都是在主項目裏使用標準的方式啓動ProxyActivity,再在ProxyActivity的生命週期裏同步調用插件中的Activity實例的生命週期方法,從而執行插件APK的業務邏輯。框架

ProxyActivity + 沒註冊的Activity = 標準的Activity

下面談談代理模式是怎麼處理上面提到的兩個問題的。

處理插件Activity的生命週期

目前還真的沒什麼辦法可以處理這個問題,一個Activity的啓動,若是不採用標準的Intent方式,沒有經歷過Android系統Framework層級的一系列初始化和註冊過程,它的生命週期方法是不會被系統調用的(除非你可以修改Android系統的一些代碼,而這已是另外一個領域的話題了,這裏不展開)。

那把插件APK裏全部Activity都註冊到主項目的Manifest裏,再以標準Intent方式啓動。可是事先主項目並不知道插件Activity裏會新增哪些Activity,若是每次有新加的Activity都須要升級主項目的版本,那不是本末倒置了,不如把插件的邏輯直接寫到主項目裏來得方便。

那就繞繞彎吧,生命週期不就是系統對Activity一些特定方法的調用嘛,那咱們能夠在主項目裏建立一個ProxyActivity,再由它去代理調用插件Activity的生命週期方法(這也是代理模式叫法的由來)。用ProxyActivity(一個標準的Activity實例)的生命週期同步控制插件Activity(普通類的實例)的生命週期,同步的方式能夠有下面兩種:

  • 在ProxyActivity生命週期裏用反射調用插件Activity相應生命週期的方法,簡單粗暴。

  • 把插件Activity的生命週期抽象成接口,在ProxyActivity的生命週期裏調用。另外,多了這一層接口,也方便主項目控制插件Activity。

這裏補充說明下,Fragment自帶生命週期,用Fragment來代替Activity開發能夠省去大部分生命週期的控制工做,可是會使得界面跳轉比較麻煩,並且Honeycomb之前沒有Fragment,沒法在API11之前的系統使用。

在插件Activity裏使用R資源

使用代理的方式同步調用生命週期的作法容易理解,也沒什麼問題,可是要使用插件裏面的res資源就有點麻煩了。簡單的說,res裏的每個資源都會在R.java裏生成一個對應的Integer類型的id,APP啓動時會先把R.java註冊到當前的上下文環境,咱們在代碼裏以R文件的方式使用資源時正是經過使用這些id訪問res資源,然而插件的R.java並無註冊到當前的上下文環境,因此插件的res資源也就沒法經過id使用了。

這個問題困擾了咱們好久,一開始的項目急於投入生產,因此咱們索性拋開res資源,插件裏須要用到的新資源都經過純Java代碼的方式建立(包括XML佈局、動畫、點九圖等),蛋疼但有效。知道網上出現瞭解決這一個問題的有效方法(一開始貌似是在手機QQ項目中出現的,可是沒有開源因此不清楚,在這裏真的佩服這些對技術這麼有追求的開發者)。

記得咱們平時怎麼使用res資源的嗎,就是「getResources().getXXX(resid)」,看看「getResources()」

@Override
    public Resources getResources() {
        if (mResources != null) {
            return mResources;
        }
        if (mOverrideConfiguration == null) {
            mResources = super.getResources();
            return mResources;
        } else {
            Context resc = createConfigurationContext(mOverrideConfiguration);
            mResources = resc.getResources();
            return mResources;
        }
    }

看起來像是經過mResources實例獲取res資源的,在找找mResources實例是怎麼初始化的,看看上面的代碼發現是使用了super類ContextThemeWrapper裏的「getResources()」方法,看進去

Context mBase;
    public ContextWrapper(Context base) {
        mBase = base;
    }
@Override
    public Resources getResources()
    {
        return mBase.getResources();
    }

看樣子又調用了Context的「getResources()」方法,看到這裏,咱們知道Context只是個抽象類,其實際工做都是在ContextImpl完成的,趕忙去ContextImpl裏看看「getResources()」方法吧

@Override
    public Resources getResources() {
        return mResources;
    }

…………
……
你TM在逗我麼,仍是沒有mResources的建立過程啊!啊,不對,mResources是ContextImpl的成員變量,多是在構造方法中建立的,趕忙去看看構造方法(這裏只給出關鍵代碼)。

resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
                        packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
                        packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
                        overrideConfiguration, compatInfo);
mResources = resources;

看樣子是在ResourcesManager的「getTopLevelResources」方法中建立的,看進去

Resources getTopLevelResources(String resDir, String[] splitResDirs,
            String[] overlayDirs, String[] libDirs, int displayId,
            Configuration overrideConfiguration, CompatibilityInfo compatInfo) {
        Resources r;
        AssetManager assets = new AssetManager();
        if (libDirs != null) {
            for (String libDir : libDirs) {
                if (libDir.endsWith(".apk")) {
                    if (assets.addAssetPath(libDir) == 0) {
                        Log.w(TAG, "Asset path '" + libDir +
                                "' does not exist or contains no resources.");
                    }
                }
            }
        }
        DisplayMetrics dm = getDisplayMetricsLocked(displayId);
        Configuration config ……;
        r = new Resources(assets, dm, config, compatInfo);
        return r;
    }

看來這裏是關鍵了,看樣子就是經過這些代碼從一個APK文件加載res資源並建立Resources實例,通過這些邏輯後就可使用R文件訪問資源了。具體過程是,獲取一個AssetManager實例,使用其「addAssetPath」方法加載APK(裏的資源),再使用DisplayMetrics、Configuration、CompatibilityInfo實例一塊兒建立咱們想要的Resources實例。

最終訪問插件APK裏res資源的關鍵代碼以下

try {  
        AssetManager assetManager = AssetManager.class.newInstance();  
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);  
        addAssetPath.invoke(assetManager, mDexPath);  
        mAssetManager = assetManager;  
    } catch (Exception e) {  
        e.printStackTrace();  
    }  
    Resources superRes = super.getResources();  
    mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),  
            superRes.getConfiguration());

注意,有的人擔憂從插件APK加載進來的res資源的ID可能與主項目裏現有的資源ID衝突,其實這種方式加載進來的res資源並非融入到主項目裏面來,主項目裏的res資源是保存在ContextImpl裏面的Resources實例,整個項目共有,而新加進來的res資源是保存在新建立的Resources實例的,也就是說ProxyActivity其實有兩套res資源,並非把新的res資源和原有的res資源合併了(因此不怕R.id重複),對兩個res資源的訪問都須要用對應的Resources實例,這也是開發時要處理的問題。(其實應該有3套,Android系統會加載一套framework-res.apk資源,裏面存放系統默認Theme等資源)

額外補充下,這裏你可能注意到了咱們採用了反射的方法調用AssetManager的「addAssetPath」方法,而在上面ResourcesManager中調用AssetManager的「addAssetPath」方法是直接調用的,不用反射啊,並且看看SDK裏AssetManager的「addAssetPath」方法的源碼(這裏也能看到具體APK資源的提取過程是在Native裏完成的),發現它也是public類型的,外部能夠直接調用,爲何還要用反射呢?

/**
     * Add an additional set of assets to the asset manager.  This can be
     * either a directory or ZIP file.  Not for use by applications.  Returns
     * the cookie of the added asset, or 0 on failure.
     * {@hide}
     */
    public final int addAssetPath(String path) {
        synchronized (this) {
            int res = addAssetPathNative(path);
            makeStringBlocks(mStringBlocks);
            return res;
        }
    }

這裏有個誤區,SDK的源碼只是給咱們參考用的,APP實際上運行的代碼邏輯在android.jar裏面(位於android-sdk\platforms\android-XX),反編譯android.jar並找到ResourcesManager類就能夠發現這些接口都是對應用層隱藏的。

public final class AssetManager{
  AssetManager(){throw new RuntimeException("Stub!"); } 
  public void close() { throw new RuntimeException("Stub!"); } 
  public final InputStream open(String fileName) throws IOException { throw new RuntimeException("Stub!"); } 
  public final InputStream open(String fileName, int accessMode) throws IOException { throw new RuntimeException("Stub!"); } 
  public final AssetFileDescriptor openFd(String fileName) throws IOException { throw new RuntimeException("Stub!"); } 
  public final native String[] list(String paramString) throws IOException;

  public final AssetFileDescriptor openNonAssetFd(String fileName) throws IOException { throw new RuntimeException("Stub!"); } 
  public final AssetFileDescriptor openNonAssetFd(int cookie, String fileName) throws IOException { throw new RuntimeException("Stub!"); } 
  public final XmlResourceParser openXmlResourceParser(String fileName) throws IOException { throw new RuntimeException("Stub!"); } 
  public final XmlResourceParser openXmlResourceParser(int cookie, String fileName) throws IOException { throw new RuntimeException("Stub!"); } 
  protected void finalize() throws Throwable { throw new RuntimeException("Stub!");
  }
  public final native String[] getLocales();
}

到此,啓動插件裏的Activity的兩大問題都有解決的方案了。

代理模式的具體項目 dynamic-load-apk

上面只是分析了代理模式的關鍵技術點,若是運用到具體項目中去的話,除了兩個關鍵的問題外,還有許多繁瑣的細節須要處理,咱們須要設計一個框架,規範插件APK項目的開發,也方便之後功能的擴展。這裏,dynamic-load-apk向咱們展現了許多優秀的處理方法,好比:

  1. 把Activity關鍵的生命週期方法抽象成DLPlugin接口,ProxyActivity經過DLPlugin代理調用插件Activity的生命週期;

  2. 設計一個基礎的BasePluginActivity類,插件項目裏使用這些基類進行開發,能夠以接近常規Android開發的方式開發插件項目;

  3. 以相似的方式處理Service的問題;

  4. 處理了大量常見的兼容性問題(好比使用Theme資源時出現的問題);

  5. 處理了插件項目裏的so庫的加載問題;

  6. 使用PluginPackage管理插件APK,從而能夠方便地管理多個插件項目;

處理插件項目裏的so庫的加載

這裏須要把插件APK裏面的SO庫文件解壓釋放出來,在根據當前設備CPU的型號選擇對應的SO庫,並使用System.load方法加載到當前內存中來,具體分析請參考 Android動態加載補充 加載SD卡的SO庫

多插件APK的管理

動態加載一個插件APK須要三個對應的DexClassLoaderAssetManagerResources實例,能夠用組合的方式建立一個PluginPackage類存放這三個變量,再建立一個管理類PluginManager,用成員變量HashMap<dexPath,pluginPackage>的方式保存PluginPackage實例。

具體的代碼請參考原項目的文檔、源碼以及Sample裏面的示例代碼,在這裏感謝singwhatiwanna的開源精神。

實際應用中可能要處理的問題

插件APK的管理後臺

使用動態加載的目的,就是但願能夠繞過APK的安裝過程升級應用的功能,若是插件APK是打包在主項目內部的那動態加載純粹是屢次一舉。更多的時候咱們但願能夠在線下載插件APK,而且在插件APK有新版本的時候,主項目要從服務器下載最新的插件替換本地已經存在的舊插件。爲此,咱們應該有一個管理後臺,它大概有如下功能:

  1. 上傳不一樣版本的插件APK,並向APP主項目提供插件APK信息查詢功能和下載功能;

  2. 管理在線的插件APK,並能向不一樣版本號的APP主項目提供最合適的插件APK;

  3. 萬一最新的插件APK出現緊急BUG,要提供舊版本回滾功能;

  4. 出於安全考慮應該對APP項目的請求信息作一些安全性校驗;

插件APK合法性校驗

加載外部的可執行代碼,一個逃不開的問題就是要確保外部代碼的安全性,咱們可不但願加載一些來歷不明的插件APK,由於這些插件有的時候能訪問主項目的關鍵數據。

最簡單可靠的作法就是校驗插件APK的MD5值,若是插件APK的MD5與咱們服務器預置的數值不一樣,就認爲插件被改動過,棄用。

是熱部署,仍是插件化?

這一部分做爲補充說明,若是不太熟悉動態加載的使用姿式,可能不是那麼容易理解。

談到動態加載的時候咱們常常說到「熱部署」和「插件化」這些名詞,它們雖然都和動態加載有關,可是仍是有一點區別,這個問題涉及到主項目與插件項目的交互方式。前面咱們說到,動態加載方式,能夠在「項目層級」作到代碼分離,按道理咱們但願是主項目和插件項目不要有任何交互行爲,實際上也應該如此!這樣作不只能確保項目的安全性,也能簡化開發工做,因此通常的作法是

只有在用戶使用到的時候才加載插件

主項目仍是像常規Android項目那樣開發,只有用戶使用插件APK的功能時才動態加載插件並運行,插件一旦運行後,與主項目沒有任何交互邏輯,只有在主項目啓動插件的時候才觸發一次調用插件的行爲。好比,咱們的主項目裏有幾款推廣的遊戲,平時在用戶使用主項目的功能時,能夠先靜默把遊戲(其實就是一個插件APK)下載好,當用戶點擊遊戲入口時,以動態加載的方式啓動遊戲,遊戲只運行插件APK裏的代碼邏輯,結束後返回主項目界面。

一啓動主項目就加載插件

另一種徹底相反的情形是,主項目只提供一個啓動的入口,以及從服務器下載最新插件的更新邏輯,這兩部分的代碼都是長期保持不變的,應用一啓動就動態加載插件,全部業務邏輯的代碼都在插件裏實現。好比如今一些遊戲市場都要求開發者接入其SDK項目,若是SDK項目採用這種開發方式,先提供一個空殼的SDK給開發者,空殼SDK能從服務器下載最新的插件再運行插件裏的邏輯,就能保證開發者開發的遊戲每次啓動的時候都能運行最新的代碼邏輯,而不用讓開發者在SDK有新版本的時候從新更換SDK並構建新的遊戲APK。

讓插件使用主項目的功能

明明,說了不要交互的,恰恰,Android開發者就是這麼執着於技術。

有些時候,好比,主項目裏有一個成熟的圖片加載框架ImageLoader,而插件裏也有一個ImageLoader。若是一個應用同時運行兩套ImageLoader,那會有許多額外的性能開銷,若是能讓插件也用主項目的ImageLoader就行了。另外,若是在插件裏須要用到用戶登陸功能,咱們總不但願用戶使用主項目時進行一次登陸,進入插件時由來一次登陸,若是能在插件裏使用主項目的登陸狀態就行了。

所以,有些時候咱們但願插件項目能調用主項目的功能。怎麼處理好呢,因爲插件項目與主項目是分開的,咱們在開發插件的時候,怎麼調用主項目的代碼啊?這裏須要稍微瞭解一下Android項目間的依賴方式。

想一想一個普通的APK是怎麼構建和運行的,Android SDK提供了許多系統類(如Activity、Fragment等,通常咱們也喜歡在這裏查看源碼),咱們的Android項目依賴Android SDK項目並使用這些類進行開發,那構建APK的時候會把這些類打包進來嗎?不會,要是每一個APK都打包一份,那得有多少冗餘啊。因此Android項目至少有兩種依賴的方式,一種構建時會把被依賴的項目(Library)的類打包進來,一種不會。

在Android Studio打開項目的Project Structure,找到具體Module的Dependencies選項卡

能夠看到Library項目有個Scope屬性,這裏的Compile模式就是會把Library的類打包進來,而Provided模式就不會。

注意,使用Provided模式的Library只能是jar文件,而不能是一個Android Library項目,由於後者可能自帶了一些res資源,這些資源沒法一併塞進標準的jar文件裏面。到這裏咱們明白,Android SDK的代碼實際上是打包進系統ROM(俗稱Framework層級)裏面的,咱們開發Android項目的時候,只是以Provided模式引用android.jar,從這個角度也佐證了上面談到的「爲何APP實際運行時AssetManager類的邏輯會與Android SDK裏的源碼不同」。

如今好辦了,若是要在插件裏使用主項目的ImageLoader,咱們能夠把ImageLoader的相關代碼抽離成一個Android Libary項目,主項目以Compile模式引用這個Libary,而插件項目以Provided模式引用這個Library(編譯出來的jar),這樣能實現二者之間的交互了,固然代價也是明顯的。

  1. 咱們應該只給插件開放一些必要的接口,否則會有安全性問題;

  2. 做爲通用模塊的Library應該保持不變(起碼接口不變),否則主項目與插件項目的版本同步會複雜許多;

  3. 由於插件項目已經嚴重依賴主項目了,因此插件項目不能獨立運行,由於缺乏必要的環境

最後咱們再說說「熱部署」和「插件化」的區別,通常咱們把獨立運行的插件APK叫熱部署,而須要依賴主項目的環境運行的插件APK叫作插件化。

相關文章
相關標籤/搜索