Android插件化技術(shù)入門

插件化概述

提到插件化,就不得不提起方法數(shù)超過(guò)65535的問(wèn)題,我們可以通過(guò)Dex分包來(lái)解決,同時(shí)也可以通過(guò)使用插件化開(kāi)發(fā)來(lái)解決。插件化的概念就是由宿主APP去加載以及運(yùn)行插件APP。

下面是一些插件化的優(yōu)勢(shì):

  • 在一個(gè)大的項(xiàng)目里面,為了明確的分工,往往不同的團(tuán)隊(duì)負(fù)責(zé)不同的插件APP,這樣分工更加明確。
  • 各個(gè)模塊封裝成不同的插件APK,不同模塊可以單獨(dú)編譯,提高了開(kāi)發(fā)效率。
  • 解決了上述的方法數(shù)超過(guò)限制的問(wèn)題。
  • 可以通過(guò)上線新的插件來(lái)解決線上的BUG,達(dá)到“熱修復(fù)”的效果。
  • 減小了宿主APK的體積。

下面是插件化開(kāi)發(fā)的缺點(diǎn):

  • 插件化開(kāi)發(fā)的APP不能在Google Play上線,也就是沒(méi)有海外市場(chǎng)。

綜上所述,如果您的APP不需要支持海外的話,還是可以考慮插件化開(kāi)發(fā)的。

插件化、熱修復(fù)(思想)的發(fā)展歷程

  • 2012年7月,AndroidDynamicLoader,大眾點(diǎn)評(píng),陶毅敏:思想是通過(guò)Fragment以及schema的方式實(shí)現(xiàn)的,這是一種可行的技術(shù)方案,但是還有限制太多,這意味這你的activity必須通過(guò)Fragment去實(shí)現(xiàn),這在activity跳轉(zhuǎn)和靈活性上有一定的不便,在實(shí)際的使用中會(huì)有一些很奇怪的bug不好解決,總之,這還是一種不是特別完備的動(dòng)態(tài)加載技術(shù)。
  • 2013年,23Code,自定義控件的動(dòng)態(tài)下載:主要利用 Java ClassLoader 的原理,可動(dòng)態(tài)加載的內(nèi)容包括 apk、dex、jar等。
  • 2014年初,Altas,阿里伯奎的技術(shù)分享:提出了插件化的思想以及一些思考的問(wèn)題,相關(guān)資料比較少。
  • 2014年底,Dynamic-load-apk,任玉剛:動(dòng)態(tài)加載APK,通過(guò)Activity代理的方式給插件Activity添加生命周期。
  • 2015年4月,OpenAltas/ACCD:Altas的開(kāi)源項(xiàng)目,一款強(qiáng)大的Android非代理動(dòng)態(tài)部署框架,目前已經(jīng)處于穩(wěn)定狀態(tài)。
  • 2015年8月,DroidPlugin,360的張勇:DroidPlugin 是360手機(jī)助手在 Android 系統(tǒng)上實(shí)現(xiàn)了一種新的插件機(jī)制:通過(guò)Hook思想來(lái)實(shí)現(xiàn),它可以在無(wú)需安裝、修改的情況下運(yùn)行APK文件,此機(jī)制對(duì)改進(jìn)大型APP的架構(gòu),實(shí)現(xiàn)多團(tuán)隊(duì)協(xié)作開(kāi)發(fā)具有一定的好處。
  • 2015年9月,AndFix,阿里:通過(guò)NDK的Hook來(lái)實(shí)現(xiàn)熱修復(fù)。
  • 2015年11月,Nuwa,大眾點(diǎn)評(píng):通過(guò)dex分包方案實(shí)現(xiàn)熱修復(fù)。
  • 2015年底,Small,林光亮:打通了宿主與插件之間的資源與代碼共享。
  • 2016年4月,ZeusPlugin,掌閱:ZeusPlugin最大特點(diǎn)是:簡(jiǎn)單易懂,核心類只有6個(gè),類總數(shù)只有13個(gè)。

下面是插件化框架的一些對(duì)比,下面引用https://github.com/wequick/Small/blob/master/Android/COMPARISION.md。

插件化框架對(duì)比.png
插件化框架對(duì)比.png
[1] 獨(dú)立插件:一個(gè)完整的apk包,可以獨(dú)立運(yùn)行。比如從你的程序跑起淘寶、QQ,但這加載起來(lái)是要鬧哪樣?
     非獨(dú)立插件:依賴于宿主,宿主是個(gè)殼,插件可使用其資源代碼并分離之以最小化,這才是業(yè)務(wù)需要嘛。
     -- “所有不能加載非獨(dú)立插件的插件化框架都是耍流氓”。
[2] ACDD加載.so用了Native方法(libdexopt.so),不是Java層,源碼見(jiàn)dexopt.cpp。
[3] Service更新頻度低,可預(yù)先注冊(cè)在宿主的manifest中,如果沒(méi)有很好的理由說(shuō)服我,現(xiàn)不支持。
[4] 要實(shí)現(xiàn)宿主、各個(gè)插件資源可互相訪問(wèn),需要對(duì)他們的資源進(jìn)行分段處理以避免沖突。
[5] 這些框架修改aapt源碼、重編、覆蓋SDK Manager下載的aapt,我只想說(shuō)_“殺(wan)雞(de)焉(kai)用(xin)牛(jiu)刀(hao)”。Small使用gradle-small-plugin,在后期修改二進(jìn)制文件,實(shí)現(xiàn)了PP_段分區(qū)。
[6] 使用public-padding對(duì)資源id的_TT_段進(jìn)行分區(qū),分開(kāi)了宿主和插件。但是插件之間無(wú)法分段。
[7] 除了宿主提供一些公共資源與代碼外,我們?nèi)孕璺庋b一些業(yè)務(wù)層面的公共庫(kù),這些庫(kù)被其他插件所依賴。公共插件打包的目的就是可以單獨(dú)更新公共庫(kù)插件,并且相關(guān)插件不需要?jiǎng)拥健?[8] AppCompat: Android Studio默認(rèn)添加的主題包,Google主推的Metrial Design包也依賴于此。大勢(shì)所趨。
[9] 聯(lián)調(diào)插件:使用Android Studio調(diào)試宿主時(shí),可直接在插件代碼中添加斷點(diǎn)調(diào)試。
插件化框架對(duì)比.png

插件化的原理

通過(guò)上面的框架介紹,插件化的原理無(wú)非就是這些:

  1. 通過(guò)DexClassLoader加載。
  2. 代理模式添加生命周期。
  3. Hook思想跳過(guò)清單驗(yàn)證。

插件化需要掌握一些系統(tǒng)底層的知識(shí),比如說(shuō)IPC,Android系統(tǒng)、APP、四大組件的啟動(dòng)過(guò)程,APK的安裝過(guò)程。

插件化實(shí)戰(zhàn)體驗(yàn)

通過(guò)DexClassLoader加載這個(gè)插件APK

下面寫(xiě)一個(gè)簡(jiǎn)單的例子,僅起到拋磚引玉的作用。

首先我們需要有一個(gè)插件APK,我們?cè)诶锩娣湃胍粋€(gè)類:

package com.nan.plugin;

/**
 * Created by huannan on 2017/6/20.
 */

public class Bean {

    private String name = "璐寶寶";

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

然后在宿主APP里面,通過(guò)DexClassLoader加載這個(gè)插件APK,并且通過(guò)反射實(shí)例化Bean并調(diào)用Bean的方法。

public class MainActivity extends AppCompatActivity {

    private ClassLoader mPluginClassLoader;

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);

        try {
            //把Assets里面的文件復(fù)制到 /data/data/包名/files 目錄下
            //注意:不同手機(jī)廠商可能目錄不一樣
            Utils.extractAssets(newBase, "plugin-debug.apk");
        } catch (Throwable throwable) {

        }
    }

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

        //插件APK路徑
        //  /data/user/0/com.nan.dynalmic/files/plugin-debug.apk
        String dexPath = getFileStreamPath("plugin-debug.apk").getAbsolutePath();
        //DexClassLoader加載的時(shí)候Dex文件釋放的路徑
        //  /data/user/0/com.nan.dynalmic/app_dex
        String fileReleasePath = getDir("dex", Context.MODE_PRIVATE).getAbsolutePath();

        Log.e("acy", dexPath);
        Log.e("acy", fileReleasePath);

        //通過(guò)DexClassLoader加載插件APK
        mPluginClassLoader = new DexClassLoader(dexPath, fileReleasePath, null, getClassLoader());

        //通過(guò)反射調(diào)用插件的代碼
        findViewById(R.id.btn_1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    Class<?> beanClass = mPluginClassLoader.loadClass("com.nan.plugin.Bean");
                    Object beanObject = beanClass.newInstance();

                    Method setNameMethod = beanClass.getMethod("setName", String.class);
                    setNameMethod.setAccessible(true);
                    Method getNameMethod = beanClass.getMethod("getName");
                    getNameMethod.setAccessible(true);

                    setNameMethod.invoke(beanObject, "huannan");
                    String name = (String) getNameMethod.invoke(beanObject);

                    Toast.makeText(MainActivity.this, name, Toast.LENGTH_SHORT).show();

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

這里需要注意的一點(diǎn)就是,我們最好先把經(jīng)過(guò)驗(yàn)證的插件APK復(fù)制到宿主APP的files目錄下面,這樣保證了APK的安全性。然后通過(guò)DexClassLoader進(jìn)行加載的時(shí)候,需要指定插件APK的路徑以及解壓之后的dex存放路徑。

通過(guò)面向接口(抽象)編程調(diào)用插件的代碼

上文介紹了通過(guò)反射調(diào)用插件的代碼,為了簡(jiǎn)化代碼提高可讀性,這里引入面向接口(抽象)編程的思想。

首先我們需要添加一個(gè)pluginlibrary,我們的app以及plugin模塊都要引用這個(gè)庫(kù)pluginlibrary,如下圖所示:

項(xiàng)目架構(gòu)

可以看到,我們?cè)趐luginlibrary里面添加了IBean接口:

public interface IBean {

    String getName();

    void setName(String name);

}

然后plugin里面的Bean類實(shí)現(xiàn)這個(gè)接口,最后在宿主加載的時(shí)候,直接把創(chuàng)建的對(duì)象轉(zhuǎn)換為這個(gè)接口就可以,省去了反射的一系列繁瑣操作,這也就是一種面向接口(抽象)編程的思想:

//通過(guò)面向接口編程調(diào)用插件的代碼

Class<?> beanClass = mPluginClassLoader.loadClass("com.nan.plugin.Bean");
IBean bean = (IBean) beanClass.newInstance();

bean.setName("test");
Toast.makeText(MainActivity.this, bean.getName(), Toast.LENGTH_SHORT).show();

通過(guò)面向切面程調(diào)用插件中的帶回調(diào)方法

比如說(shuō)現(xiàn)在插件里面有一個(gè)方法methodWithCallback,它被調(diào)用的時(shí)候,最終會(huì)回調(diào)宿主APP。

先在pluginlibrary添加一個(gè)接口專門用于宿主與插件的交互的:

public interface IDynamic {

    void methodWithCallback(Callback callback);

}

其中的Callback是自定義的一個(gè)簡(jiǎn)單的接口:

public interface Callback {

    void callback(IBean bean);

}

這個(gè)IDynamic的實(shí)現(xiàn)類由插件來(lái)實(shí)現(xiàn):

public class Dynamic implements IDynamic {

    @Override
    public void methodWithCallback(Callback callback) {
        Bean bean = new Bean();
        bean.setName("璐寶寶");

        //回調(diào)宿主APP的方法
        callback.callback(bean);
    }

}

這樣我們就可以通過(guò)回調(diào)的方式實(shí)現(xiàn)了插件調(diào)用宿主的方法了。最終宿主的調(diào)用如下:

Class<?> dynamicClass = mPluginClassLoader.loadClass("com.nan.plugin.Dynamic");
IDynamic dynamic = (IDynamic) dynamicClass.newInstance();

dynamic.methodWithCallback(new Callback() {
    @Override
    public void callback(IBean bean) {
        //插件回調(diào)宿主
        Toast.makeText(MainActivity.this, bean.getName(), Toast.LENGTH_SHORT).show();
    }
});

宿主訪問(wèn)插件的資源文件

如果我們直接去加載插件的資源的話,就會(huì)報(bào)如下錯(cuò)誤:

android.content.res.Resources$NotFoundException: String resource ID #0x7f060022

因?yàn)椴寮馁Y源沒(méi)有被Android系統(tǒng)加載進(jìn)來(lái),那么我們就需要手動(dòng)加載資源,主要是重寫(xiě)下面三個(gè)方法:

@Override
public AssetManager getAssets() {
    return mAssetManager == null ? super.getAssets() : mAssetManager;
}

@Override
public Resources getResources() {
    return mResources == null ? super.getResources() : mResources;
}

@Override
public Resources.Theme getTheme() {
    return mTheme == null ? super.getTheme() : mTheme;
}

然后在合適的時(shí)機(jī)調(diào)用loadPluginResources方法來(lái)加載插件的資源:

/**
 * 加載插件的資源:通過(guò)AssetManager添加插件的APK資源路徑
 */
protected void loadPluginResources() {
    //反射加載資源
    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());
    mTheme = mResources.newTheme();
    mTheme.setTo(super.getTheme());

}
注:有關(guān)Android資源加載機(jī)制的可以參考《Android源碼與設(shè)計(jì)模式》這本書(shū)。

最后,我們就可以訪問(wèn)到插件的資源了(這里只給出核心代碼):

Class<?> dynamicClass = mPluginClassLoader.loadClass("com.nan.plugin.Dynamic");
IDynamic dynamic = (IDynamic) dynamicClass.newInstance();

String res = dynamic.methodWithResources(MainActivity.this);
Log.e(TAG, res);

無(wú)需驗(yàn)證啟動(dòng)Activity

我們可以利用Hook機(jī)制來(lái)啟動(dòng)一個(gè)沒(méi)有在清單文件中注冊(cè)的插件Activity。但是需要我們熟悉Activity的啟動(dòng)流程。

相關(guān)的文章:http://www.itdecent.cn/p/69bfbda302df

寫(xiě)在最后

通過(guò)上面的例子我們體驗(yàn)了插件式開(kāi)發(fā)的精髓,學(xué)習(xí)這些例子是為了更好的研究市面上的插件化框架,了解它們實(shí)現(xiàn)原理,明白這些框架對(duì)項(xiàng)目以及插件的侵入性、修改這些框架以適應(yīng)自己的項(xiàng)目等。當(dāng)然有興趣的可以自己做一個(gè)。

在學(xué)習(xí)插件化的時(shí)候,需要掌握Android系統(tǒng)的一些Framework層面的知識(shí)以及一些編程相關(guān)的知識(shí),其中包括:

  • Binder機(jī)制
  • Android系統(tǒng)、APP、Activity等四大組件的啟動(dòng)流程
  • APK安裝過(guò)程
  • Android資源的加載過(guò)程
  • Hook機(jī)制
  • 面向接口(抽象)編程
  • 面向切面編程
  • 等等

相關(guān)的參考資料有:

如果覺(jué)得我的文字對(duì)你有所幫助的話,歡迎關(guān)注我的公眾號(hào):

公眾號(hào):Android開(kāi)發(fā)進(jìn)階

我的群歡迎大家進(jìn)來(lái)探討各種技術(shù)與非技術(shù)的話題,有興趣的朋友們加我私人微信huannan88,我拉你進(jìn)群交(♂)流(♀)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容