插件化解析

一邓嘹、定義&理解

插件:Plug-in又稱add-in膜赃、addon或add-on,也稱外掛,它是一種遵循一定規(guī)范的應用程序接口編寫出來的程序茫负。
我的理解就是:插件是一個程序的輔助或者擴展功能模塊破停,對程序來說插件可有可無翅楼,但它能給予程序一定的額外功能。

二真慢、背景

android插件化的概念始于2012年毅臊,出自于免安裝的想法。發(fā)展到后來黑界,android應用程序更多的需要依賴一些第三方庫管嬉,比如地圖sdk、分享sdK朗鸠、支付sdk等等蚯撩,導致安裝包變得越來越大,單是依賴這些sdk烛占,安裝包可能就會額外的增加20-30M的大星蟛蕖;當需要新增功能時,不得不重新更新整個安裝包呀癣。所以這時插件化的需求就變得更為更為突出。
插件化主要是解決的是減少應用程序大小弦赖、免安裝擴展功能项栏。一般具一定規(guī)模的程序應用插件化才有意義。

三蹬竖、原理

插件本身是一個獨立的apk文件沼沈,要實現(xiàn)插件動態(tài)加載運行,需要了解apk的運行機制币厕。java虛擬機運行的是class文件列另,使用ClassLoader加載;而Android虛擬機運行的是dex文件使用的是DexClassLoader旦装,而資源是存在xml文件中页衙,所以就會涉及到資源文件的加載過程,所以要實現(xiàn)插件的動態(tài)加載運行阴绢,首先就需要解決類文件店乐、資源、庫等的加載呻袭。

1眨八、類加載

android運行的是dex文件對應的類加載器是PathClassLoader和DexClassLoader,PathClassLoader和DexClassLoader均繼承自BaseDexClassLoader左电,BaseDexClassLoader又繼承自ClassLoader廉侧。
我們來看下DexClassLoader基類BaseDexClassLoader的構造函數(shù):

    /**
     * @param dexPath 目標類所在的apk或jar文件路徑,類加載器將從這個apk或jar文件路徑查找目標類
     * @param optimizedDirectory 從apk中解壓出dex文件篓足,經過odex優(yōu)化之后段誊,將解壓和優(yōu)化后的dex文件存放到optimizedDirectory 目錄,下次直接從這個目錄中的dex文件加載目標類纷纫。注意從API26開始此參數(shù)已經廢棄
     * @param librarySearchPath  目標類所使用的c/c++庫路徑
     * @param parent 父類ClassLoader
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
         // 解壓枕扫、加載dex、資源文件辱魁,保存存放dex烟瞧、資源文件、庫文件路徑等
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);

        if (reporter != null) {
            reportClassLoaderChain();
        }
    }

問題1:為什么要傳父類的ClassLoader染簇,有什么用参滴?
PathClassLoader的構造函數(shù):

 public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
 }

從PathClassLoader的注釋(自己看下PathClassLoader源碼的注釋)可以知道PathClassLoader是用于加載系統(tǒng)和應用內部類的。
問題2:為什么PathClassLoader是用于加載系統(tǒng)和應用內部的锻弓?有什么依據(jù)砾赔?

DexClassLoader的構造函數(shù):

public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        // 注意API26開始optimizedDirectory設置為空,API26之前是new File(optimizedDirectory)
        super(dexPath, null, librarySearchPath, parent);
}

從DexClassLoader的注釋自己看下DexClassLoader源碼的注釋)可以知道,DexClassLoader可以加載從指定路徑(包括Sd卡)加載apk暴心、jar妓盲、dex文件,實現(xiàn)動態(tài)加載功能专普,所以插件化類的加載就是通過DexClassLoader實現(xiàn)悯衬。
我們來看下類加載的過程:

# ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // 首先檢查類是否已經加載過(可以防止重復加載,提高效率)
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    // 未加載過檀夹,則優(yōu)先從父類的ClassLoader加載
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // 在從根ClassLoader加載筋粗,android中返回空
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                   //父類加載失敗,則嘗試調用自身的findClass方法加載
                    c = findClass(name);
                }
            }
            return c;
    }

通過上面類加載的過程可以知道類的加載優(yōu)先使用父類的ClassLoader進行加載炸渡,如果父類加載失敗娜亿,才通過自身去加載,這個過程即使有個術語蚌堵,叫雙親委派买决。為什么要這樣做呢?主要是為了防止重復加載辰斋,提高效率策州。(這里回答了問題1)

2、資源加載

android中四大組件獲取資源一般都是通過getResources()得到Resources對象宫仗,通過Resources對象來獲取各種資源(文本够挂、字體、顏色等)藕夫,而Resources中獲取各種資源實際上都是通過AssetManager來實現(xiàn)的孽糖。我們來看下getString的代碼實現(xiàn)過程:

#Resources.java
public String getString(@StringRes int id) throws NotFoundException {
        return getText(id).toString();
}
public CharSequence getText(@StringRes int id) throws NotFoundException {
        CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
        if (res != null) {
            return res;
        }
        throw new NotFoundException("String resource ID #0x"
                + Integer.toHexString(id));
}

通過上面的代碼知道Resources的實現(xiàn)類是ResourceImpl類,getAssets()返回的是AssetManager毅贮,所以也就證實了資源的加載實際是通過AssetManager來加載的办悟。

3、android四大組件加載

上面講述了類和資源的加載原理滩褥,由于Android中的四大組件均有相應的生命周期病蛉,由系統(tǒng)管理,所以如果單純的只是通過類加載器來加載一個組件瑰煎,是無法實現(xiàn)生命周期相關的功能铺然。
要怎么實現(xiàn)插件中的組件也具有相應的生命周期呢?下面我們以Activity為實例來講解
首先先要知道什么時候酒甸、如何加載Activity并創(chuàng)建Activity實例魄健,并且什么時候創(chuàng)建Resources對象、如何將Resources與Activity建立關系的插勤。
我們通過Activity的創(chuàng)建過程來一步一步來揭開謎底:

#ActivityThread.java
//啟動Activity
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        //...省略
       // 生成Context的實現(xiàn)類
        ContextImpl appContext = createBaseContextForActivity(r);
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            //...省略
        }
        //...省略
        activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback);
        //...省略
    }

#Instrumentation.java
public Activity newActivity(ClassLoader cl, String className,
            Intent intent)
            throws InstantiationException, IllegalAccessException,
            ClassNotFoundException {
        return (Activity)cl.loadClass(className).newInstance();
}

// ContextImpl.java類加載器
@Override
public ClassLoader getClassLoader() {
        return mClassLoader != null ? mClassLoader : (mPackageInfo != null ?       mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader());
}

#ClassLoader.java
public static ClassLoader getSystemClassLoader() {
        return SystemClassLoader.loader;
}
static private class SystemClassLoader {
        public static ClassLoader loader = ClassLoader.createSystemClassLoader();
} 

private static ClassLoader createSystemClassLoader() {
        String classPath = System.getProperty("java.class.path", ".");
        String librarySearchPath = System.getProperty("java.library.path", "");
        return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}

通過上面的代碼我們知道:

  • Context的實現(xiàn)類是ContextImpl類
  • Activity實例是在Instrumentation中的newActivity中通過ClassLoader的實現(xiàn)類加載創(chuàng)建的沽瘦。
  • 加載Activity使用的ClassLoader是PathClassLoader革骨。(回答了問題2)

問題3:資源對象是什么時候創(chuàng)建的?
問題4:如何與Activity建立聯(lián)系的呢析恋?
我們先看下Activity中getResouces()的代碼實現(xiàn):

    #Activity.java
    @Override
    public Resources getResources() {
        return getResourcesInternal();
    }

    private Resources getResourcesInternal() {
        if (mResources == null) {
            if (mOverrideConfiguration == null) {
                mResources = super.getResources();
            } else {
                final Context resContext = createConfigurationContext(mOverrideConfiguration);
                mResources = resContext.getResources();
            }
        }
        return mResources;
    }

通過getResouces()方法知道良哲,Activity中Resources對象是通過Context獲取的,而上面Activity的創(chuàng)建過程中Context是通過Activity的attach方法進行綁定的(回答了問題4)绿满。
既然知道了Reources是通過Context的獲取的臂外,那么我們看下Context實現(xiàn)類ContextImpl中Resources的創(chuàng)建過程:

#ActivityThread.java
private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
  //...省略
  ContextImpl appContext = ContextImpl.createActivityContext(
                this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);
  //...省略
}

#ContextImpl.java
static ContextImpl createActivityContext(ActivityThread mainThread,
            LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
            Configuration overrideConfiguration) {
      //...省略
      //創(chuàng)建ContextImpl實例
      ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName,
                activityToken, null, 0, classLoader);
      //...省略
      // 創(chuàng)建ResourcesManager 對象
      final ResourcesManager resourcesManager = ResourcesManager.getInstance();

        // Create the base resources for which all configuration contexts for this Activity
        // will be rebased upon.
        //通過ResourcesManager 對象生成Resources對象并賦值給Context中的Resources
        context.setResources(resourcesManager.createBaseActivityResources(activityToken,
                packageInfo.getResDir(),
                splitDirs,
                packageInfo.getOverlayDirs(),
                packageInfo.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfiguration,
                compatInfo,
                classLoader));
      //...省略
}

#ResourcesManager.java
public @Nullable Resources createBaseActivityResources(@NonNull IBinder activityToken,
            @Nullable String resDir,
            @Nullable String[] splitResDirs,
            @Nullable String[] overlayDirs,
            @Nullable String[] libDirs,
            int displayId,
            @Nullable Configuration overrideConfig,
            @NonNull CompatibilityInfo compatInfo,
            @Nullable ClassLoader classLoader) {
       //...省略
      //獲取或者創(chuàng)建Resources對象
      return getOrCreateResources(activityToken, key, classLoader);
       //...省略
}

private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
            @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
      //...省略
      ResourcesImpl resourcesImpl = createResourcesImpl(key);
      //...省略
}

private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
        final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
        daj.setCompatibilityInfo(key.mCompatInfo);
        // 創(chuàng)建AssetManager對象
        final AssetManager assets = createAssetManager(key);
        if (assets == null) {
            return null;
        }

        final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
        final Configuration config = generateConfig(key, dm);
        //創(chuàng)建Resources實現(xiàn)類對象
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);

        if (DEBUG) {
            Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
        }
        return impl;
}

@VisibleForTesting
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        AssetManager assets = new AssetManager();

        // resDir can be null if the 'android' package is creating a new Resources object.
        // This is fine, since each AssetManager automatically loads the 'android' package
        // already.
        if (key.mResDir != null) {
            //添加資源文件目錄
            if (assets.addAssetPath(key.mResDir) == 0) {
                Log.e(TAG, "failed to add asset path " + key.mResDir);
                return null;
            }
       //...省略
}

通過上面的代碼,我們知道Activity中的Resouces對象是在生成ContextImpl時創(chuàng)建的喇颁,而且是通過Resources的資源加載是通過AssetManager實現(xiàn)的,而資源文件目錄是通過AssetManager的addAssetPath來實現(xiàn)的嚎货。(回答了問題3)

結合上面的代碼分析橘霎,知道了Activity是什么時候、在哪里殖属、如何創(chuàng)建的姐叁;頁知道了Activity中資源是怎么創(chuàng)建和建立關系的,那有什么方案可以解決插件中Activity的生命周期的問題呢洗显?
目前市面主要有兩個方案:通過代理或通過hook外潜。
問題5:什么是hook?

3.1、代理

問題6:代理的思想是什么或者如何實現(xiàn)插件Activity的生命周期挠唆?
代理的思想是在主工程放一個ProxyActivity处窥,啟動插件的Activity時先啟動ProxyActivity,再在ProxyActivity中創(chuàng)建插件Activity玄组,并同步生命周期給插件的Activity(解答了問題6)滔驾,看下其原理圖:


圖1

缺點

  • 插件中的Activity必須繼承PluginActivity,開發(fā)侵入性強俄讹。
  • 如果想支持Activity的singleTask哆致,singleInstance等啟動模式,需要自己管理Activity棧患膛,實現(xiàn)起來很繁瑣摊阀。
  • 插件中需要小心處理Context,容易出錯踪蹬。
  • 如果想把之前的模塊改造成插件需要很多額外的工作胞此。
3.2、Hook:

Hook:使用技術手段在運行時動態(tài)的將額外代碼依附給現(xiàn)進程延曙,從而實現(xiàn)替換現(xiàn)有處理邏輯或插入額外功能的目的
可以理解為Hook通過代理豌鹤、反射等機制實現(xiàn)代碼的注入,從而實現(xiàn)對原來功能的改寫枝缔,以達到預想中的效果
此處回答了問題5
我們再看下Activity的整體啟動流程圖:


圖2

結合Activity創(chuàng)建過程的代碼分析和上圖的流程中布疙,我們可以通過hook步驟1和步驟10來實現(xiàn)插件Activity的生命周期等相關功能蚊惯。代理的方式時因為浸入性太強,hook的方式需要考慮浸入性的問題灵临,其次我們在開發(fā)應用的時候截型,如果Activity不再Manifest中注冊,當正常方式啟動Activity時就會拋出找不到Activity的錯誤信息儒溉,所以
通過hook的方式啟動插件Activity需要解決如下問題:
a宦焦、插件Activity如何繞開Manifest中注冊的檢測
b、如何創(chuàng)建Activity實例顿涣,并同步生命周期
我們通過VirtualApk插件化框架來看其實現(xiàn)方案:
a波闹、預先在Manifest中注冊各種啟動模式的Activity占坑,啟動時hook第1步涛碑,將Intent根據(jù)啟動模式替換成預先在Manifest占坑的Activity精堕,這樣就解決了Manifest中注冊的檢測
b、hook第10步蒲障,使用插件的ClassLoader反射創(chuàng)建插件Activity歹篓,之后Activity的生命周期回調都通知給插件Activity,這樣就解決了創(chuàng)建Activity并同步生命周期的問題

3.2.1揉阎、替換系統(tǒng)Instrumentation

VirtualApk在初始化時會調用hookInstrumentationAndHandler()庄撮,該方法hook了系統(tǒng)的Instrumentation(該類與Activity啟動相關):

protected void hookInstrumentationAndHandler() {
        try {
            // 獲取ActivityThread對象
            ActivityThread activityThread = ActivityThread.currentActivityThread();
            // 獲取ActivityThread中的Instrumentation
            Instrumentation baseInstrumentation = activityThread.getInstrumentation();
            // 創(chuàng)建自定義的VAInstrumentation,具有復寫父類一些函數(shù)且具有代理功能
            final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);
            // 反射的機制替換ActivityThread中的Instrumentation
            Reflector.with(activityThread).field("mInstrumentation").set(instrumentation);
            Handler mainHandler = Reflector.with(activityThread).method("getHandler").call();
            Reflector.with(mainHandler).field("mCallback").set(instrumentation);
            this.mInstrumentation = instrumentation;
            Log.d(TAG, "hookInstrumentationAndHandler succeed : " + mInstrumentation);
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }

上面代碼毙籽,創(chuàng)建自定義的Instrumentation洞斯,通過反射替換了ActivityThread中的Instrumentation

3.2.2、使用自定義統(tǒng)Instrumentation啟動Activity

復寫3.2.1自定義Instrumentation的execStartActivity惧财,

@Override
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Fragment target, Intent intent, int requestCode, Bundle options) {
        injectIntent(intent);
        return mBase.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
}

protected void injectIntent(Intent intent) {
        // 處理隱身啟動巡扇,如果匹配到插件Activity,則隱身啟動替換為顯示啟動
        mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
        // null component is an implicitly intent
        if (intent.getComponent() != null) {
            Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(), intent.getComponent().getClassName()));
            // resolve intent with Stub Activity if needed
            //如果是插件Activity,將Intent中的ClassName替換為占坑中的Activity,解決啟動時是否
            //在Manifest中注冊的校驗
            this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
        }
    }

public void markIntentIfNeeded(Intent intent) {
        if (intent.getComponent() == null) {
            return;
        }

        String targetPackageName = intent.getComponent().getPackageName();
        String targetClassName = intent.getComponent().getClassName();
        // search map and return specific launchmode stub activity
        if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
            intent.putExtra(Constants.KEY_IS_PLUGIN, true);
            intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
            intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
            dispatchStubActivity(intent);
        }
    }

execStartActivity時垮衷,先處理隱身啟動厅翔,如果匹配到插件Activity則替換為顯示啟動;接著通過markIntentIfNeeded將待啟動的插件Activity替換為預先在Manifest占坑的Activity搀突,并將插件Activity信息保存在Intent中刀闷。其中有個dispatchStubActivity函數(shù),會根據(jù)Activity的launchMode選擇具體啟動哪個StubActivity仰迁。VirtualAPK為了支持Activity的launchMode在主工程的AndroidManifest中對于每種啟動模式的Activity都預埋了多個坑位甸昏。

3.2.3、hook創(chuàng)建Activity

上一步欺騙了系統(tǒng)徐许,讓系統(tǒng)以為啟動的是一個正常的Activity施蜜。這一步切換回插件的Activity,調用VAInstrumentation的newActivity:

@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        try {
            cl.loadClass(className);
            Log.i(TAG, String.format("newActivity[%s]", className));
            
        } catch (ClassNotFoundException e) {
            ComponentName component = PluginUtil.getComponent(intent);
            
            if (component == null) {
                return newActivity(mBase.newActivity(cl, className, intent));
            }
          
            String targetClassName = component.getClassName();
            Log.i(TAG, String.format("newActivity[%s : %s/%s]", className, component.getPackageName(), targetClassName));
            //根據(jù)component獲取對應的插件
            LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(component);
            // 插件對象為空時雌隅,使用默認的創(chuàng)建方式
            if (plugin == null) {
                // Not found then goto stub activity.
                boolean debuggable = false;
                try {
                    Context context = this.mPluginManager.getHostContext();
                    debuggable = (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
                } catch (Throwable ex) {
        
                }
    
                if (debuggable) {
                    throw new ActivityNotFoundException("error intent: " + intent.toURI());
                }
                
                Log.i(TAG, "Not found. starting the stub activity: " + StubActivity.class);
                return newActivity(mBase.newActivity(cl, StubActivity.class.getName(), intent));
            }
            //使用插件的ClassLoader
            Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
            activity.setIntent(intent);
            
            //設置activity的資源對象
            // for 4.1+
            Reflector.QuietReflector.with(activity).field("mResources").set(plugin.getResources());
            //返回創(chuàng)建的Activity
            return newActivity(activity);
        }

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

由于Manifest中注冊的占坑Activity沒有實現(xiàn)類翻默,所以這里會報ClassNotFoundException 異常缸沃,在處理異常中取插件Activity的信息,使用插件ClassLoader反射創(chuàng)建插件Activity

3.2.4修械、設置Activity的資源Resources對象

插件Activity的構造創(chuàng)建之后趾牧,還需要做一些額外的操作方才可以正常運行,比如設置訪問資源所使用的Resources對象肯污、Context等:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle, PersistableBundle persistentState) {
        injectActivity(activity);
        mBase.callActivityOnCreate(activity, icicle, persistentState);
}

protected void injectActivity(Activity activity) {
        final Intent intent = activity.getIntent();
        if (PluginUtil.isIntentFromPlugin(intent)) {
            
            Context base = activity.getBaseContext();
            try {
                //根據(jù)Intent獲取對應的插件對象
                LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
                //反射設置Resources對象為插件Resources
                Reflector.with(base).field("mResources").set(plugin.getResources());
                Reflector reflector = Reflector.with(activity);
                // 反射設置Context為插件的Context, 其實就是主工程的Context
                reflector.field("mBase").set(plugin.createPluginContext(activity.getBaseContext()));
                // 反射設置Application為插件的Application,其實就是主工程的Application
                reflector.field("mApplication").set(plugin.getApplication());
                
                //設置橫豎屏
                // set screenOrientation
                ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
                if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
                    activity.setRequestedOrientation(activityInfo.screenOrientation);
                }
                
                //重新設置Intent
                // for native activity
                ComponentName component = PluginUtil.getComponent(intent);
                Intent wrapperIntent = new Intent(intent);
                wrapperIntent.setClassName(component.getPackageName(), component.getClassName());
                activity.setIntent(wrapperIntent);
                
            } catch (Exception e) {
                Log.w(TAG, e);
            }
        }
    }

這段代碼主要是將Activity中的Resource翘单,Context等對象替換成了插件的相應對象,保證插件Activity在調用涉及到Context的方法時能夠正確運行蹦渣。

至此便實現(xiàn)了插件Activity的啟動哄芜,且此插件Activity中并不需要什么額外的處理,和常規(guī)的Activity一樣柬唯。那問題來了忠烛,之后的onResume,onStop等生命周期怎么辦呢权逗?答案是所有和Activity相關的生命周期函數(shù),系統(tǒng)都會調用插件中的Activity冤议。原因在于AMS在處理Activity時斟薇,通過一個token表示具體Activity對象,而這個token正是和啟動Activity時創(chuàng)建的對象對應的恕酸,而這個Activity被我們替換成了插件中的Activity堪滨,所以之后AMS的所有調用都會傳給插件中的Activity。

其他組件

四大組件中Activity的支持是最復雜的蕊温,其他組件的實現(xiàn)原理要簡單很多袱箱,簡要概括如下:

  • Service:Service和Activity的差別在于,Activity的生命周期是由用戶交互決定的义矛,而Service的生命周期是我們通過代碼主動調用的发笔,且Service實例和manifest中注冊的是一一對應的。實現(xiàn)Service插件化的思路是通過在manifest中預埋StubService凉翻,hook系統(tǒng)startService等調用替換啟動的Service了讨,之后在StubService中創(chuàng)建插件Service,并手動管理其生命周期制轰。
  • BroadCastReceiver:解析插件的manifest前计,將靜態(tài)注冊的廣播轉為動態(tài)注冊。
  • ContentProvider:類似于Service的方式垃杖,對插件ContentProvider的所有調用都會通過一個在manifest中占坑的ContentProvider分發(fā)男杈。

七、插件間调俘、插件與主程序間的交互

上面解決了插件化的原理伶棒,那么插件與插件之間旺垒、插件與主工程之間如何相互調用、相互訪問資源呢?
相互調用對應的是類的調用苞冯,而每個類都需要對應的ClassLoader來加載袖牙,所以一般就分為兩種調用機制:

  • 單ClassLoader機制:將插件中DexClassLoader的pathList合入主工程的pathList中,這樣主工程就可以調用插件的類和方法舅锄,插件調用另一個插件則需要借助主工程或者插件框架統(tǒng)一訪問接口來實現(xiàn)鞭达。單ClassLoader的弊端就是如果插件使用不同版本的庫就會出現(xiàn)問題。
  • 多ClassLoader機制:每個插件對應一個DexClassLoader皇忿,那么主工程調用插件的類和方法就需要借助插件的ClassLoader畴蹭,一般需要進行插件化框架進行統(tǒng)一管理。
    資源的訪問也是有兩種方式:
  • 合入式:將插件的資源路徑合入主工程的AssetManager中鳍烁,因此生成的Resources可以同時訪問插件和主工程的資源叨襟,弊端是由于插件和主工程是獨立編譯的,所有會存在資源id沖突的情況(解決資源Id沖突需要通過更改編譯過程修改資源Id幔荒,資源id是由8位16進制數(shù)表示糊闽,表示為0xPPTTNNNN。PP段用來區(qū)分包空間爹梁,默認只區(qū)分了應用資源和系統(tǒng)資源右犹,TT段為資源類型,NNNN段在同一個APK中從0000遞增姚垃。其中系統(tǒng)的pp段的值是0x01,應用pp段的值是0x7f;系統(tǒng)和應用的tt段值都是0x04,剩下的NNNN值在0x0000至0xfffff之間)
  • 獨立式:資源隔離念链,不存在沖突情況,但是資源共享比較麻煩积糯,需要借助統(tǒng)一接口進行管理掂墓,會存在資源在不同插件重復存在的情況。

八看成、幾種成熟的插件框架

圖3

第一代:dynamic-load-apk最早使用ProxyActivity這種靜態(tài)代理技術君编,由ProxyActivity去控制插件中PluginActivity的生命周期。該種方式缺點明顯绍昂,插件中的activity必須繼承PluginActivity啦粹,開發(fā)時要小心處理context。而DroidPlugin通過Hook系統(tǒng)服務的方式啟動插件中的Activity窘游,使得開發(fā)插件的過程和開發(fā)普通的app沒有什么區(qū)別唠椭,但是由于hook過多系統(tǒng)服務,異常復雜且不夠穩(wěn)定忍饰。

第二代:為了同時達到插件開發(fā)的低侵入性(像開發(fā)普通app一樣開發(fā)插件)和框架的穩(wěn)定性贪嫂,在實現(xiàn)原理上都是趨近于選擇盡量少的hook,并通過在manifest中預埋一些組件實現(xiàn)對四大組件的插件化艾蓝。另外各個框架根據(jù)其設計思想都做了不同程度的擴展力崇,其中Small更是做成了一個跨平臺斗塘,組件化的開發(fā)框架。

第三代:VirtualApp比較厲害亮靴,能夠完全模擬app的運行環(huán)境馍盟,能夠實現(xiàn)app的免安裝運行和雙開技術。Atlas是阿里今年開源出來的一個結合組件化和熱修復技術的一個app基礎框架茧吊,其廣泛的應用與阿里系的各個app贞岭,其號稱是一個容器化框架。

九搓侄、參考

深入理解Android插件化技術

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末瞄桨,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子讶踪,更是在濱河造成了極大的恐慌芯侥,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乳讥,死亡現(xiàn)場離奇詭異柱查,居然都是意外死亡,警方通過查閱死者的電腦和手機云石,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進店門物赶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人留晚,你說我怎么就攤上這事「娉埃” “怎么了错维?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長橄唬。 經常有香客問我赋焕,道長,這世上最難降的妖魔是什么仰楚? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任隆判,我火速辦了婚禮,結果婚禮上僧界,老公的妹妹穿的比我還像新娘侨嘀。我一直安慰自己,他們只是感情好捂襟,可當我...
    茶點故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布咬腕。 她就那樣靜靜地躺著,像睡著了一般葬荷。 火紅的嫁衣襯著肌膚如雪涨共。 梳的紋絲不亂的頭發(fā)上纽帖,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天,我揣著相機與錄音举反,去河邊找鬼懊直。 笑死,一個胖子當著我的面吹牛火鼻,可吹牛的內容都是我干的室囊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼凝危,長吁一口氣:“原來是場噩夢啊……” “哼波俄!你這毒婦竟也來了?” 一聲冷哼從身側響起蛾默,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤懦铺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后支鸡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體冬念,經...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年牧挣,在試婚紗的時候發(fā)現(xiàn)自己被綠了急前。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡瀑构,死狀恐怖裆针,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情寺晌,我是刑警寧澤世吨,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站呻征,受9級特大地震影響耘婚,放射性物質發(fā)生泄漏。R本人自食惡果不足惜陆赋,卻給世界環(huán)境...
    茶點故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一沐祷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧攒岛,春花似錦赖临、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春色乾,著一層夾襖步出監(jiān)牢的瞬間誊册,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工暖璧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留案怯,地道東北人。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓澎办,卻偏偏與公主長得像嘲碱,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子局蚀,可洞房花燭夜當晚...
    茶點故事閱讀 44,689評論 2 354

推薦閱讀更多精彩內容