手動實現(xiàn)一次插件化

為什么學(xué)習(xí)插件化

在項目迭代過程中民效,業(yè)務(wù)愈加復(fù)雜,在單工程開發(fā)模型下炬守,業(yè)務(wù)模塊耦合度極高牧嫉,模塊邊界模糊,對工程所做的任何修改都必須編譯整個工程减途,團(tuán)隊協(xié)同暴露出諸多沖突與不便酣藻,不得不向多工程開發(fā)模型發(fā)展。代表為組件化鳍置、插件化辽剧。

多工程開發(fā)模型的核心點在于,能像搭積木一樣靈活地將各個模塊組裝形成一個可獨自編譯運行的小工程税产,而最終的APP怕轿,則是由這些小工程聚合而成。這樣辟拷,團(tuán)隊成員可以專注于各自的小工程里撞羽,互不干涉,邊界清晰衫冻。

組件化與插件化如何做選擇

如果就日常開發(fā)的角度看诀紊,兩者并沒有什么不同,比如均可實現(xiàn)業(yè)務(wù)組件的熱插拔隅俘,并且遇到的問題也是一致的邻奠,比如需要解決分層問題、跳轉(zhuǎn)問題为居、跨模塊通信問題等等惕澎。但從賦予能力與技術(shù)實現(xiàn)上的角度來看,可將組件化看成插件化的子集颜骤。插件化包含了組件化所有的優(yōu)點,并實現(xiàn)了動態(tài)化捣卤。

插件化解決的核心問題如下:

  • 動態(tài)化發(fā)布
  • 可以看到dex分包和資源分包方案

因此忍抽,如果面對的業(yè)務(wù)不存在快速迭代頻繁發(fā)布的需求,插件化的威力將減少大半董朝。并且鸠项,動態(tài)化往往伴隨兼容性與穩(wěn)定性問題,因此還需衡量團(tuán)隊面對此等問題的付出與產(chǎn)出是否值得子姜。

文章目的

雖然已有各種插件化框架祟绊,并學(xué)習(xí)知悉插件化原理,但,紙上得來終覺淺牧抽,絕知此事要躬行嘉熊。通過手動實現(xiàn)一次插件化,加深對插件化原理的理解扬舒。并且在平日學(xué)習(xí)到的各種Framework層的知識阐肤,也可在此過程中得到印證。

note: 文章源碼基于8.0

如何實現(xiàn)插件化

插件化的動態(tài)化實際要解決三個問題:

  • 類加載
  • 資源加載
  • 四大組件的管理

類加載

當(dāng)使用到插件中的類時讲坎,需要先將類進(jìn)行加載才可使用孕惜。因此,需要知道Android下的類加載原理晨炕。


類加載器.png

圖片來源

類加載原理可參考: 好怕怕的類加載器

簡單來說衫画,使用類加載器通過雙親委任模型對類進(jìn)行加載,特點如下:

  • 每個類加載器實例化時需要傳入另一個類加載器作為父加載器
  • 一個類是否被加載過根據(jù) ClassLoader + PackageName + ClassName 來進(jìn)行判斷
  • 加載類時會讓父加載器先進(jìn)行加載瓮栗,如果父加載器不加載削罩,則自己再加載,這樣能保證上層類如Framework層的類能直接使用遵馆,避免重復(fù)加載鲸郊,還可以隔離核心類庫被修改
  • PathClassLoader 負(fù)責(zé)加載系統(tǒng)的類和主dex中的類
  • DexClassLoader 可從包含 Classes.dex 的jar包或APK中加載類

資源加載

類的使用往往涉及對資源的使用,圖片货邓、布局文件等等秆撮。插件里的資源未進(jìn)行裝載,而當(dāng)訪問到時换况,必然crash职辨,因此需要將插件的資源進(jìn)行加載。


資源加載原理.png

資源加載原理可參考:Android 資源加載機(jī)制剖析

資源加載原理可以描述為:

  • APK打包時戈二,通過aapt將所有資源規(guī)整出arsc文件舒裤,arsc文件包含了所有資源ID、資源類型觉吭、文件路徑信息和所有字符串信息
  • 通過AssetManager.addAssetPath()傳入APK路徑腾供,最終觸發(fā)Nativie層AssetMananger.cpp.appendPathToResTable()創(chuàng)建ResTable
  • Java層訪問資源時,通過資源ID和ReTable鲜滩,可以得知資源的描述信息TypeValue伴鳖,即可從中拿到資源的關(guān)鍵信息并進(jìn)行訪問

四大組件的管理

對于四大組件來說,裝載了相應(yīng)類并沒有達(dá)到可用狀態(tài)徙硅。之后均以Activity來做說明榜聂。

以new方式創(chuàng)建Activity是不行的,Activity的運行需要具備相應(yīng)的上下文嗓蘑,并需要在AndroidManifest文件里進(jìn)行注冊须肆。APK在安裝時匿乃,PMS從AndroidManifest里收集所有Activity的信息(當(dāng)然還有其它信息,這里省略)豌汇,等在Activity啟動時幢炸,AMS通過PMS獲取Activity信息,如啟動模式瘤礁、所在進(jìn)程等阳懂,再進(jìn)行啟動。

在插件化的場景下柜思,使用Activity需要解決三個問題:
1岩调、Activity的啟動需要上下文
2、在AndroidManifest里進(jìn)行注冊
3赡盘、通過AMS的校驗

Activity的啟動原理可參考: Activity啟動時發(fā)生了什么

手動實現(xiàn)

知道了插件化所需解決的問題号枕,也粗略了解了相應(yīng)問題涉及的原理,也就可以見招拆招陨享,開始簡單地手動實現(xiàn)插件化葱淳。

第一步,合并DEX

在類加載原理的基礎(chǔ)上抛姑,Android中的ClassLoader實例化時接收APK文件路徑赞厕,從中解析出Dex,并存于BaseDexClassLoader.dexPathList.dexElements定硝,以便需要類加載時從中獲取類信息皿桑。
源碼可見
DexClassLoader()
-> BaseDexClassLoader()
-> DexPathList()
-> DexPathList.makeDexElements()

    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader) {
    ......
    // 解析出dex
    DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
    if (dex != null) {
        // 將dex存儲elments,之后此elements的所有元素將存于DexPathList.dexElements
        elements[elementsPos++] = new Element(dex, null);
    }
    ......
}

了解原理思路就明了了蔬啡。通過新建類加載器诲侮,為插件APK解析出Element,并將其插入宿主的Element箱蟆,即可提供插件的類信息沟绪,如圖


插件化-合并dex.png

代碼實現(xiàn)如下

    public static void loadPluginDex(Application context, ClassLoader classLoader) throws Exception{

        // 獲取插件apk
        String apkPath =  getPatchApkPath(context);
        File apkFile = new File(apkPath);

        // 創(chuàng)建安裝插件的Classloader
        DexClassLoader dexClassLoader = new DexClassLoader(apkFile.getAbsolutePath(), null, null,classLoader);

        // 獲取BaseDexClassLoader.dexPathList
        Object pluginDexPatchList = ReflectUtil.getField(dexClassLoader, "pathList");
        // 獲取DexFile.dexElements
        Object pluginDexElements =  ReflectUtil.getField(pluginDexPatchList, "dexElements");

        // 通過反射獲取宿主 dexPathList
        Object hostDexPatchList = ReflectUtil.getField(classLoader, "pathList");
        // 通過反射獲取宿主 dexElements
        Object hostDexElements =  ReflectUtil.getField( hostDexPatchList, "dexElements");

        // 合并dexElements
        Object array = combineArray(hostDexElements, pluginDexElements);
        ReflectUtil.setField( hostDexPatchList, "dexElements", array);
        
        // 載入資源文件
        mergePluginResources(context);
    }

第二步,加載插件資源

日常開發(fā)時空猜,訪問資源往往通過 context.getResources().xxx(R.xxx.xxx)進(jìn)行訪問绽慈,而getResources()獲得的Resources對象存于ContextImpl.mResources,此對象還存于LoadedApk.packageInfo辈毯。

源碼可見
ActivityThread.handleLaunchActivity()
-> ActivityThread.performLaunchActivity()
-> LoadedApk.makeApplication()

    // LoadedAPK
    public Application makeApplication(boolean forceDefaultAppClass,
            Instrumentation instrumentation) {
            ......
            ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
            app = mActivityThread.mInstrumentation.newApplication(
                    cl, appClass, appContext);
            appContext.setOuterContext(app);
            ......
    }
    
    // ContextImpl
    static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
        if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
                null);
        // 設(shè)置mResource
        context.setResources(packageInfo.getResources());
        return context;
    }

因此實現(xiàn)方式是久信,通過AssetManaget從APK中將資源載入生成新的Resources,再替換ContextImpl.mResources以及ContextImpl.mPackageInfo漓摩。
實現(xiàn)代碼為:

    public static void loadPluginResources(Application application) throws Exception{
        AssetManager assetManager = AssetManager.class.newInstance();
        // 獲取 AssetManager.addAssetPath() 方法
        Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
        // 載入插件的資源
        addAssetPath.invoke(assetManager, getPatchApkPath(application));

        // 創(chuàng)建新的Resource對象
        Resources merResource = new Resources(assetManager,
                application.getBaseContext().getResources().getDisplayMetrics(),
                application.getBaseContext().getResources().getConfiguration());

        // 替換 ContextImpl.mResources
        ReflectUtil.setField(
                application.getBaseContext(),
                "mResources",
                merResource);

        // 獲取 ContextImpl 中的 LoadedApk 類型的 mPackageInfo
        Field mPackageInfoField = application.getBaseContext().getClass().getDeclaredField("mPackageInfo");
        mPackageInfoField.setAccessible(true);
        Object packageInfoO = mPackageInfoField.get(application.getBaseContext());
        // 替換 mPackageInfo.mResources
        ReflectUtil.setField(packageInfoO, "mResources", merResource);

        // 替換 ContextImpl 中的 Resources.Theme
        Field themeField = application.getBaseContext().getClass().getDeclaredField("mTheme");
        themeField.setAccessible(true);
        themeField.set(application.getBaseContext(), null);
    }

第三步,管理插件Actvity

Activity啟動過程入客,調(diào)用椆鼙校可以簡要描述成下圖


Activity啟動調(diào)用棧.png

1腿椎、注冊問題
Activity需要在AndroidManifest進(jìn)行注冊才可使用,且無法進(jìn)行動態(tài)注冊夭咬,那么想要使用插件中未注冊的Activity啃炸,此步驟也無法省略。常規(guī)的做法是卓舵,使用一個替身StubActivity在AndroidManifest里進(jìn)行注冊南用,以達(dá)到占位效果,所有插件的Activity均通過StubActivity共同欺騙AMS掏湾。
2裹虫、通過AMS校驗
既然替身StubActivity已經(jīng)進(jìn)行過正常注冊,必然能經(jīng)過AMS的校驗融击。問題是筑公,使用StubActivity代替實際Activity通過AMS校驗,就需要在合適的時機(jī)將實際Activity裝扮成StubActivity尊浪,同樣在合適時機(jī)將其還原匣屡。
3、上下文
提升StubActivity跟AMS打交道時拇涤,能拿到相應(yīng)的Context捣作。Activity的啟動過程中,在Activity.attach()時機(jī)將Context進(jìn)行綁定鹅士。因此保證ActivityThread構(gòu)建出的Activity為實際需要的Activity券躁,自然可以拿到Context。

綜上所訴如绸,問題可以簡化成嘱朽,需要一個替身來通過AMS檢測并在合適時機(jī)還原。因此存在動態(tài)替換方案與靜態(tài)替換方案怔接。后續(xù)做了三種實現(xiàn)方式搪泳。

方式一,Hook Instrumentation

Instumentation.execStartActivity() 可視為啟動Activity的起點扼脐,可作為裝扮實際Activity的節(jié)點岸军。當(dāng)ActivityThread加載Activity類時,則通過Instumentation.newActivity()進(jìn)行加載瓦侮,所以此處可作為恢復(fù)真正Activity的節(jié)點艰赞。

在AndroidManifest里注冊StubActivity,不需要創(chuàng)建類文件

<activity android:name=".StubActivity" />

創(chuàng)建自己的Instrumentation類肚吏,進(jìn)行實現(xiàn)方妖,關(guān)鍵代碼如下

public class InstrumentationHook extends Instrumentation {
        public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {

        if (!StubHelper.checkActivity(who, intent)){
             // 保存要啟動的插件Activity的信息,使用StubActivity做偽裝
            intent.putExtra(REAL_ACTIVITY_BANE, intent.getComponent().getClassName());
            intent.setClassName(who, StubHelper.STUB_ACTIVITY);
        }

        try {
            // 通過實際的mInstrumentation進(jìn)行啟動
            return (ActivityResult) startActivityMethod.invoke(mInstrumentation, who, contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {

        }
        return null;
    }
    
        @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        String startActivityName = intent.getStringExtra(REAL_ACTIVITY_BANE);
        if (!TextUtils.isEmpty(startActivityName)) {
            // 還原Activity
            return super.newActivity(cl, startActivityName, intent);
        }

        return super.newActivity(cl, className, intent);
    }
    
    ......
}

再將Instrumentation進(jìn)行替換罚攀,這部分代碼省略党觅。

方式二雌澄,代理AMS

啟動Activity過程,可是視起點為AMS.startActivity()杯瞻,且AMS最后通過向ActivityThread發(fā)送信息镐牺,在ActivityThread.H接收到LAUNCH_ACTIVITY信號后,將創(chuàng)建Activity魁莉,也代表Activity通過檢驗睬涧。選取這兩處作為替換、還原節(jié)點旗唁。

在APP內(nèi)畦浓,AMS以IActivityManager形式存在,通過代理IActivityManager則能達(dá)到代理AMS的目的逆皮。在8.0的源碼里宅粥,代理IActivityManager,則需按照以下步驟:

  • 獲取ActivityManager.getService()电谣,通過此方法獲得IActivityManagerSingleton
  • IActivityManagerSingleton類型為Singleton秽梅,Singleton為系統(tǒng)提供單例實現(xiàn)輔助類,實例存于Singleton.mInstance剿牺,因此IActivityManager存于mInstance
  • 代理IActivityManager

代碼實現(xiàn)如下

    private static void replaceActivity(final Context context) throws Exception{

        // 通過ActivityManager獲取AMS實例, , 26以上有效
        Class amClass = Class.forName("android.app.ActivityManager");
        Method getServiceMethod = amClass.getDeclaredMethod("getService");
        final Object iActivityManagerObje = getServiceMethod.invoke(null);
        Field iActivityManagerSingletonField = amClass.getDeclaredField("IActivityManagerSingleton");
        Object iActivityManagerSingletonObj = ReflectUtil.getStaticField(amClass, "IActivityManagerSingleton");

        // 獲取 mInstance
        Class singleTonClass = Class.forName("android.util.Singleton");
        Field mInstanceField = singleTonClass.getDeclaredField("mInstance");
        mInstanceField.setAccessible(true);

        iActivityManagerSingletonField.setAccessible(true);

        // ams 實例
        final Object amsObj = ReflectUtil.getField(iActivityManagerSingletonObj, "mInstance");

        // 獲取IActivityManager類
        Class<?> iamClass = Class.forName("android.app.IActivityManager");
        // 創(chuàng)建IActivityManager的動態(tài)代理
        Object proxy = Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class<?>[]{iamClass},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        // hook startActivity
                        if (!"startActivity".equals(method.getName())){
                            return method.invoke(amsObj, args);
                        }

                        // 找到intent對象
                        int intentIndex= 0;
                        for (int i=0; i<args.length; i++){
                            if (args[i] instanceof Intent){
                                intentIndex = i;
                                break;
                            }
                        }

                        Intent realIntent = (Intent) args[intentIndex];

                        // 檢查啟動的Activity是否在宿主Manifest聲明
                        if (StubHelper.checkActivity(context, realIntent)){
                            return method.invoke(amsObj, args);
                        }

                        // 使用占坑的Activity繞過AMS企垦,替換Intent
                        Intent stubIntent = new Intent();
                        // SELF_PAK為插件APK包名
                        stubIntent.setComponent(new ComponentName(StubHelper.SELF_PAK, StubHelper.STUB_ACTIVITY));
                        stubIntent.putExtra(StubHelper.REAL_INTENT, realIntent);
                        args[intentIndex] = stubIntent;

                        return method.invoke(amsObj, args);
                    }
                }
        );

        // 代理ams
        mInstanceField.setAccessible(true);
        mInstanceField.set(iActivityManagerSingletonObj, proxy);
    }

以上完成了偽裝成StubActivity的功能。在觸發(fā)創(chuàng)建Activity的節(jié)點晒来,通過ActivityThread.H來接收觸發(fā)钞诡,而H為Handler,因此湃崩,可以為Handler設(shè)置Callback荧降,在接收到LAUNCH_ACTIVITY信號時,恢復(fù)成真正的Activity攒读。代碼實現(xiàn)如下:

    private static void restoreActivity() throws Exception{
        // 獲取 ActivityThread
        Class atClass = Class.forName("android.app.ActivityThread");
        Object curAtObj = ReflectUtil.getStaticField(atClass, "sCurrentActivityThread");

        // 獲取 ActivityThread 中的 handle , 即 mH
        final Handler mHObj = (Handler) ReflectUtil.getField(curAtObj, "mH");

        // 設(shè)置 Handler 的 mCallBack
        Class handlerClass = Handler.class;
        ReflectUtil.setField(mHObj, "mCallback", new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                try{
                    int LAUNCH_ACTIVITY = 0;
                    Class hClass = Class.forName("android.app.ActivityThread$H");
                    LAUNCH_ACTIVITY = (int) ReflectUtil.getStaticField(hClass, "LAUNCH_ACTIVITY");

                    if (msg.what == LAUNCH_ACTIVITY){
                        // 恢復(fù)原來的intent
                        Intent intent = (Intent) ReflectUtil.getField(msg.obj, "intent");
                        Intent realIntent = intent.getParcelableExtra(StubHelper.REAL_INTENT);
                        if (realIntent != null){
                            intent.setComponent(realIntent.getComponent());
                        }
                    }
                } catch (Exception e){

                }
                mHObj.handleMessage(msg);
                return true;
            }
        });
    }

動態(tài)代理的插件APK

自行管理Instrumentation或代理AMS的方式屬于動態(tài)替換方案朵诫,為其準(zhǔn)備插件APK。插件APK需注意薄扁,繼承類為Activity剪返。且替換了Resources,因此邓梅,在插件APK的Activity脱盲,暫需重寫getResource方法如下

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

插件APK可來源于網(wǎng)絡(luò),但為了簡單起見日缨,直接從本地讀取钱反。只需將APK存放于宿主有權(quán)限讀取的文件路徑即可杉畜。我的情況存于context.getExternalCacheDir().getAbsolutePath()路徑下蜒秤,路徑為

/storage/emulated/0/Android/data/你的宿主包名/cache

通過adb命令

adb push 本地文件路徑 手機(jī)存儲路徑

即可將插件APK推到手機(jī)

方式三

此方式屬于靜態(tài)代理壹堰,較好理解谐岁,不需要Hook任何Framework層代碼。分三步執(zhí)行:

  1. 在宿主里創(chuàng)建ProxyActivity幢竹,以此Activity來代理生命周期
  2. 插件里以BasePluginActivity為基類創(chuàng)建Activity,當(dāng)然恩静,這些Activity并不是真正的Activity焕毫,只是看起來像Activity
  3. 啟動Activity實際上也是啟動ProxyActivity,然后將ProxyActivity與插件Activity進(jìn)行雙向綁定驶乾,搜集插件Activity的回調(diào)方法邑飒。在生命周期回調(diào)時,通過ProxyActivity傳達(dá)回調(diào)信息給插件Activity级乐,也就是通過調(diào)用插件Activity的具體方法
插件化靜態(tài)代理實現(xiàn).png

BasePluginActivity如下:

public abstract class BasePluginActivity {
    // 宿主
    protected Activity mHost;

    // 建立與代理Activity的連接
    public void proxy(Activity host){
        mHost = host;
    }

    public void setContentView(int layoutId){
        mHost.setContentView(layoutId);
    }
    
    protected abstract void onCreate(Bundle savedInstanceState);
    protected void onStart(){};
    protected void onRestart(){};
    protected void onResume(){};
    protected void onPause(){};
    protected void onStop(){};
    protected void onDestroy(){};
}

ProxyAcitivty關(guān)鍵代碼如下:

public class ProxyActivity extends Activity {

    // 插件 Activity實例
    private Object mPluginActivity;
    // 插件 Activity類名
    private String mPluginClassName;
    // 生命周期方法回調(diào)
    private Map<String, Method> mLifecycleMethods = new HashMap<>();

    public static final String PLUGIN_STUB = "plugin_stub";

    // 插件Activity類名
    public static final String PLUGIN_CLASS_NAME = "com.bf.qinx.cosplayplugin.PluginActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 獲取插件類名
        mPluginClassName = getIntent().getStringExtra(PLUGIN_STUB);

        // 代理插件
        proxyPluginActivity();
        // 執(zhí)行插件Activity.onCreate()
        invokeLifecycleMethod("onCreate", new Object[]{savedInstanceState});
    }
    
        /**
     * 代理插件Activity
     */
    private void proxyPluginActivity() {
        try{
            // 獲取插件Activity
            Class<?> clazz = Class.forName(mPluginClassName);
            Constructor<?> con = clazz.getConstructor(new Class[]{});
            mPluginActivity = con.newInstance(new Object[]{});

            // 觸發(fā)插件的hook點疙咸,建立鏈接,即調(diào)用BasaPlauginActivity.proxy()
            Method proxyMethod = clazz.getMethod("proxy", new Class[]{Activity.class});
            proxyMethod.setAccessible(true);
            proxyMethod.invoke(mPluginActivity, new Object[]{ this });

            // 收集插件Activity其它生命周期方法风科,就不展開了
            proxyLifecycle(clazz);
        } catch (Exception e){
            e.printStackTrace();
        }
    }
    
    ......
}

建立好了ProxyActivity與插件Activity的連接并收集到相應(yīng)方法存于mLifecycleMethods后撒轮,在ProxyActivity生命周期出發(fā)時贼穆,從mLifecycleMethods觸發(fā)代理Activity相應(yīng)方法即可题山,舉個例如onResume()

    @Override
    protected void onResume() {
        super.onResume();
        invokeLifecycleMethod("onResume", null);
    }

    private void invokeLifecycleMethod(String methodName, Object[] args){
        try{
            Object[] methodArgs = args;
            if (methodArgs == null){
                methodArgs = new Object[]{};
            }
            Method method = mLifecycleMethods.get(methodName);
            if (method != null){
                method.invoke(mPluginActivity, methodArgs);
            }
        } catch (Exception e){
            Log.d("xx", "invokeLifcycleMethod: " + e.getMessage());
        }
    }

啟動插件Activity

以下依次為hook ams、hook instrumentation故痊、靜態(tài)替換的啟動Activity方式

    private void startPatchActivityFormAMS(){
        Intent intent = new Intent();
        ComponentName componentName = new ComponentName("這里是插件包名" , PATCH_ACTIVITY);
        intent.setComponent(componentName);
        startActivity(intent);
    }

    private void statPatchActivityFromInstrumentation(){
        Intent intent = new Intent();
        ComponentName componentName = new ComponentName(MainActivity.this , PATCH_ACTIVITY);
        intent.setComponent(componentName);
        startActivity(intent);
    }

    private void startCosplayActivity(){
        Intent intent = new Intent();
        intent.setClass(MainActivity.this, ProxyActivity.class);
        intent.putExtra(ProxyActivity.PLUGIN_STUB, ProxyActivity.PLUGIN_CLASS_NAME);
        startActivity(intent);
    }

總結(jié)

以上通過動態(tài)方案和靜態(tài)方案為方向顶瞳,提供了三種實現(xiàn)方式。其中動態(tài)方案的優(yōu)點在于愕秫,真正地去管理Activity(以Activity為例)的生命周期慨菱,并通過hook關(guān)鍵Framework代碼讓插件Activity達(dá)到可用狀態(tài)。缺點在兼容性和穩(wěn)定性存在一定風(fēng)險戴甩,需要出處理各大源碼之間的差異符喝,比如上述代理AMS時,實際源碼場景為26以上等恐。反觀靜態(tài)代理方法洲劣,實際上沒有任何穩(wěn)定性與兼容性問題,問題在于有一定的局限性课蔬,比如囱稽,當(dāng)你想更逼真地模擬一個Activity,得下一定的功夫二跋。

Demo 源碼地址: Demo源碼地址

項目結(jié)構(gòu)如下


插件化實現(xiàn)-項目結(jié)構(gòu).jpg

實現(xiàn)插件化战惊,實際要解決類加載、資源加載扎即、管理四大組件生命周期三大問題:

  1. 通過類加載機(jī)制吞获,與Android里類加載特點况凉,將插件dex插入BaseDexClassLoader.dexPathList.dexElements即可解決類加載問題
  2. 通過AssertManager將插件dex中arsc文件解析成訪問資源所需的ResTable
  3. 通過Hook AMS 或 Hook Instrumentation,以StubActivity達(dá)到通過AMS檢測的目的各拷。也可通過靜態(tài)方式刁绒,通過ProxyActivity來模擬Activity達(dá)到目的

當(dāng)然,以上的僅僅是最簡單的實現(xiàn)烤黍,避開了很多的問題知市,在實際的插件化方案實現(xiàn)要復(fù)雜的多。

通過手動實現(xiàn)一次插件化速蕊,不僅僅是對實現(xiàn)原理有更深刻的了解嫂丙,也佐證了涉及到的Framework知識,因此對類加載原理规哲、資源加載原理跟啤、Activity啟動原理也有更真切的認(rèn)識。

參考

Android插件化原理解析
Android插件化原理和實踐 (四) 之 合并插件中的資源
手把手講解 Android Hook無清單啟動Activity的應(yīng)用
Android插件化之從入門到放棄
Android 開發(fā):由模塊化到組件化(一)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末唉锌,一起剝皮案震驚了整個濱河市隅肥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌糊秆,老刑警劉巖武福,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異痘番,居然都是意外死亡捉片,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進(jìn)店門汞舱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來伍纫,“玉大人,你說我怎么就攤上這事昂芜∮ü妫” “怎么了?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵泌神,是天一觀的道長良漱。 經(jīng)常有香客問我,道長欢际,這世上最難降的妖魔是什么母市? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮损趋,結(jié)果婚禮上患久,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好蒋失,可當(dāng)我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布返帕。 她就那樣靜靜地躺著,像睡著了一般篙挽。 火紅的嫁衣襯著肌膚如雪荆萤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天铣卡,我揣著相機(jī)與錄音观腊,去河邊找鬼。 笑死算行,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的苫耸。 我是一名探鬼主播州邢,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼褪子!你這毒婦竟也來了量淌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤嫌褪,失蹤者是張志新(化名)和其女友劉穎呀枢,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體笼痛,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡裙秋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了缨伊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片摘刑。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖刻坊,靈堂內(nèi)的尸體忽然破棺而出枷恕,到底是詐尸還是另有隱情,我是刑警寧澤谭胚,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布徐块,位于F島的核電站,受9級特大地震影響灾而,放射性物質(zhì)發(fā)生泄漏胡控。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一绰疤、第九天 我趴在偏房一處隱蔽的房頂上張望铜犬。 院中可真熱鬧,春花似錦、人聲如沸癣猾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纷宇。三九已至夸盟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間像捶,已是汗流浹背上陕。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留拓春,地道東北人释簿。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像硼莽,于是被迫代替她去往敵國和親庶溶。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,562評論 2 349

推薦閱讀更多精彩內(nèi)容