APK動態加載框架(DL)解析

轉載請註明出處:http://blog.csdn.net/singwhatiwanna/article/details/39937639 (來自singwhatiwanna的csdn博客)java

前言

很久沒有發佈新的文章,此次打算髮表一下我這幾個月的一個核心研究成果:APK動態加載框架(DL)。這段時間我致力於github的開源貢獻,開源了2個比較有用且有意義的項目,一個是PinnedHeaderExpandableListView,另外一個是APK動態加載框架。具體能夠參見個人github:https://github.com/singwhatiwannaandroid

本次要介紹的是APK動態加載框架(DL),這個項目除了我之外,還有兩個共同開發者:田嘯(時之沙),宋思宇。git

爲了更好地理解本文,你須要首先閱讀Android apk動態加載機制的研究這一系列文章,分別爲:github

Android apk動態加載機制的研究
api

Android apk動態加載機制的研究(二):資源加載和activity生命週期管理
微信

另外,這個開源項目我起了個名字,叫作DL。本文中的DL均指APK動態加載框架。架構

項目地址

https://github.com/singwhatiwanna/dynamic-load-apk,歡迎star和fork。
框架

運行效果圖:
eclipse

意義

這裏說說這個開源項目的意義。首先要說的是動態加載技術(或者說插件化)在技術驅動型的公司中扮演這至關重要的角色,當項目愈來愈龐大的時候,須要經過插件化來減輕應用的內存和cpu佔用,還能夠實現熱插拔,即在不發佈新版本的狀況下更新某些模塊。ide

我幾個月前開始進行這項技術的研究,當時查詢了不少資料,沒有找到很好的開源。目前淘寶、微信等都有成熟的動態加載框架,包括apkplug,可是它們都是不開源的。還有github上有一個開源項目AndroidDynamicLoader,其思想是經過Fragment 以及 schema的方式實習的,這是一種可行的技術方案,可是還有限制太多,這意味這你的activity必須經過Fragment去實現,這在activity跳轉和靈活性上有必定的不便,在實際的使用中會有一些很奇怪的bug很差解決,總之,這仍是一種不是特別完備的動態加載技術。而後,我發現,目前針對動態加載這一塊成熟的開源基本仍是空白的,無論是國內仍是國外。而在公司內部,動態加載做爲一項核心技術,也不多是初級開發人員所可以接觸到的,因而,我決定作一個成熟點的開源,期待能填補這一塊空白。

DL功能介紹

DL支持不少特性,而這些特性使得插件的開發過程變得透明、高效。

1. plugin無需安裝便可由宿主調起。

2. 支持用R訪問plugin資源
3. plugin支持Activity和FragmentActivity(將來還將支持其餘組件)

4. 基本無反射調用 

5. 插件安裝後仍可獨立運行從而便於調試

6. 支持3種plugin對host的調用模式:
   (1)無調用(但仍然能夠用反射調用)。
   (2)部分調用,host可公開部分接口供plugin調用。 這前兩種模式適用於plugin開發者沒法得到host代碼的狀況。

   (3)徹底調用,plugin能夠徹底調用host內容。這種模式適用於plugin開發者能得到host代碼的狀況。

7. 只需引入DL的一個jar包便可高效開發插件,DL的工做過程對開發者徹底透明

8. 支持android2.x版本

架構解析

若是你們閱讀過本文頭部提到的兩篇文章,那麼對DL的架構應該有大體的瞭解,本文就再也不從頭開始介紹了,而是從以下變動的幾方面進行解析,這些優化使得DL的功能和以前比起來更增強大更加易用使用易於擴展。

1. DL對activity生命週期管理的改進

2. DL對類加載器的支持(DLClassLoader)

3. DL對宿主(host)和插件(plugin)通訊的支持

4. DL對插件獨立運行的支持

5. DL對activity隨意跳轉的支持(DLIntent)

6. DL對插件管理的支持(DLPluginManager)

其中5和6屬於增強功能,目前正在dev分支上進行開發(本文暫不介紹),其餘功能均在穩定版分支master上。

DL對activity生命週期管理的改進

你們知道,DL最開始的時候採用反射去管理activity的生命週期,這樣存在一些不便,好比反射代碼寫起來複雜,而且過多使用反射有必定的性能開銷。針對這個問題,咱們採用了接口機制,將activity的大部分生命週期方法提取出來做爲一個接口(DLPlugin),而後經過代理activity(DLProxyActivity)去調用插件activity實現的生命週期方法,這樣就完成了插件activity的生命週期管理,而且沒有采用反射,當咱們想增長一個新的生命週期方法的時候,只須要在接口中聲明一下同時在代理activity中實現一下便可,下面看一下代碼:

接口DLPlugin

public interface DLPlugin {

    public void onStart();
    public void onRestart();
    public void onActivityResult(int requestCode, int resultCode, Intent data);
    public void onResume();
    public void onPause();
    public void onStop();
    public void onDestroy();
    public void onCreate(Bundle savedInstanceState);
    public void setProxy(Activity proxyActivity, String dexPath);
    public void onSaveInstanceState(Bundle outState);
    public void onNewIntent(Intent intent);
    public void onRestoreInstanceState(Bundle savedInstanceState);
    public boolean onTouchEvent(MotionEvent event);
    public boolean onKeyUp(int keyCode, KeyEvent event);
    public void onWindowAttributesChanged(LayoutParams params);
    public void onWindowFocusChanged(boolean hasFocus);
    public void onBackPressed();
}
在代理類DLProxyActivity中的實現

...
    @Override
    protected void onStart() {
        mRemoteActivity.onStart();
        super.onStart();
    }

    @Override
    protected void onRestart() {
        mRemoteActivity.onRestart();
        super.onRestart();
    }

    @Override
    protected void onResume() {
        mRemoteActivity.onResume();
        super.onResume();
    }

    @Override
    protected void onPause() {
        mRemoteActivity.onPause();
        super.onPause();
    }

    @Override
    protected void onStop() {
        mRemoteActivity.onStop();
        super.onStop();
    }
...
說明:經過上述代碼應該不難理解DL對activity生命週期的管理,其中mRemoteActivity就是DLPlugin的實現。

DL對類加載器的支持

爲了更好地對多插件進行支持,咱們提供了一個DLClassoader類,專門去管理各個插件的DexClassoader,這樣,同一個插件就能夠採用同一個ClassLoader去加載類從而避免多個classloader加載同一個類時所引起的類型轉換錯誤。

public class DLClassLoader extends DexClassLoader {
    private static final String TAG = "DLClassLoader";

    private static final HashMap<String, DLClassLoader> mPluginClassLoaders = new HashMap<String, DLClassLoader>();

    protected DLClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(dexPath, optimizedDirectory, libraryPath, parent);
    }

    /**
     * return a available classloader which belongs to different apk
     */
    public static DLClassLoader getClassLoader(String dexPath, Context context, ClassLoader parentLoader) {
        DLClassLoader dLClassLoader = mPluginClassLoaders.get(dexPath);
        if (dLClassLoader != null)
            return dLClassLoader;

        File dexOutputDir = context.getDir("dex", Context.MODE_PRIVATE);
        final String dexOutputPath = dexOutputDir.getAbsolutePath();
        dLClassLoader = new DLClassLoader(dexPath, dexOutputPath, null, parentLoader);
        mPluginClassLoaders.put(dexPath, dLClassLoader);

        return dLClassLoader;
    }
}

DL對宿主(host)和插件(plugin)通訊的支持

這一點很重要,由於每每宿主須要和插件進行各類通訊,所以DL對宿主和插件的通訊作了很好的支持,目前總共有3中模式,以下圖所示:


下面分別介紹上述三種模式,針對上述三種模式,咱們分別提供了3組例子,其中:

1. depend_on_host:插件徹底依賴宿主的模式,適合於可以能到宿主的源代碼的狀況

其中host指宿主工程,plugin指插件工程

2. depend_on_interface:插件部分依賴宿主的模式,或者說插件依賴宿主提供的接口,適合可以拿到宿主的接口的狀況

其中host指宿主工程,plugin指插件工程,common指接口工程

3. main:插件不依賴宿主的模式,這是DL推薦的模式

其中host指宿主工程,plugin指插件工程


模式1:這是DL推薦的模式,對應的工程目錄爲main。在這種模式下,宿主和插件不須要通訊,二者是獨立開發的,宿主引用DL的jar包(dl-lib.jar),插件也須要引用DL的jar包,可是不能放入到插件工程的libs目錄下面,換句話說,就是插件編譯的時候依賴jar包可是打包成apk的時候不要把jar包打進去,這是由於,dl-lib.jar已經在宿主工程中存在了,若是插件中也有這個jar包,就會發生類連接錯誤,緣由很簡單,內存中有兩份同樣的類,重複了。至於support-v4也是一樣的道理。對於eclipse很簡單,只須要在插件工程中建立一個目錄,好比external-jars,而後把dl-lib.jar和support-v4.jar放進去,同時在.classpath中追加以下兩句便可:

<classpathentry kind="lib" path="external-jars/dl-lib.jar"/>
<classpathentry kind="lib" path="external-jars/android-support-v4.jar"/>

這樣,編譯的時候就可以正常進行,可是打包的時候,就不會把上面兩個jar包打入到插件apk中。

至於ant環境和gradle,解決辦法不同,具體方法後面再補上,可是思想都是同樣的,即:插件apk中不要打入上述2個jar包。

模式2:插件部分依賴宿主的模式,或者說插件依賴宿主提供的接口,適合可以拿到宿主的接口的狀況。在這種模式下,宿主放出一些接口並實現一些接口,而後給插件調用,這樣插件就能夠訪問宿主的一些服務等

模式3:插件徹底依賴宿主的模式,適合於可以能到宿主的源代碼的狀況。這種模式通常多用在公司內部,插件能夠訪問宿主的全部代碼,可是,這樣插件和宿主的耦合比較高,宿主一動,插件就必須動,比較麻煩


具體採用哪一種方式,須要結合實際狀況來選擇,通常來講,若是是宿主和插件不是同一個公司開發,建議選擇模式1和模式2;若是宿主和插件都在同一個公司開發,那麼選擇哪一個均可以。從DL的實現出發,咱們推薦採用模式1,真的須要通訊的話採用模式2,儘可能不要採用模式3.

DL對插件獨立運行的支持

爲了便於調試,採用DL所開發的插件均可以獨立運行,固然,這要分狀況來講:

對於模式1,若是插件想獨立運行,只須要把external-jars下的jar包拷貝一份到插件的libs目錄下便可

對於模式2,只須要提供一個宿主接口的默認實現便可

對於模式3,只須要apk打包時把所引用的宿主代碼打包進去便可,具體方式能夠參看sample/depend_on_host目錄。

在開發過程當中,應該先開啓插件的獨立運行功能以便於調試,等功能開發完畢後再將其插件化。

DLIntent和DLPluginManager

這兩項都屬於增強功能,目前正在dev分支進行code review,你們感興趣能夠去dev分支上查看,等驗證經過即merge到穩定版master分支。

DLIntent:經過DLIntent來完成activity的無約束調起

DLPluginManager:對宿主的全部插件提供綜合管理功能。

開發規範

目前DL已經達到了第一個穩定版,通過大量機型的驗證,目前得出的結論是DL是可靠的(兼容到android2.x),能夠用在實際的應用開發中。可是,咱們知道,動態加載是一個技術壁壘,其很難達到一種完美的狀態,畢竟,讓一個apk不安裝跑起來,這是多麼難以想象的事情。所以,但願你們辯證地去看這個問題,下面列出咱們目前還不支持的功能,或者說是一種開發規範吧,但願你們在開發過程當中去遵照這個規範,這樣才能讓插件穩定地跑起來。


DL 1.0開發規範

1. 目前不支持service

2. 目前只支持動態註冊廣播

3. 目前支持Activity和FragmentActivity,這也是經常使用的activity

4. 目前不支持插件中的assets

5. 調用Context的時候,請適當使用that,大部分經常使用api是不須要用that的,可是一些不經常使用api仍是須要用that來訪問。that是apk中activity的基類BaseActivity系列中的一個成員,它在apk安裝運行的時候指向this,而在未安裝的時候指向宿主程序中的代理activity,因爲that的動態分配特性,經過that去調用activity的成員方法,在apk安裝之後仍然能夠正常運行。

6. 慎重使用this,由於this指向的是當前對象,即apk中的activity,可是因爲activity已經不是常規意義上的activity,因此this是沒有意義的,可是,當this表示的不是Context對象的時候除外,好比this表示一個由activity實現的接口。


但願可以給你們帶來一些幫助,但願你們多多支持!

本開源項目地址:https://github.com/singwhatiwanna/dynamic-load-apk,歡迎你們star和fork。

相關文章
相關標籤/搜索