攜程DynamicAPK插件化框架源碼分析

jopen 8年前發布 | 34K 次閱讀 Android開發 移動開發

攜程DynamicAPK插件化框架源碼分析


Author:莫川


插件核心思想

1.aapt的改造

分別對不同的插件項目分配不同的packageId,然后對各個插件的資源進行編譯,生成R文件,然后與宿主項目的R文件進行id的合并。
要求:由于最終會將所有的資源文件id進行合并,因此,所有的資源名稱均不能相同。

2.運行ClassLoader加載各Bundle

和MultiDex的思路是一樣的,所有的插件都被加載到同一個ClassLoader當中,因此,不同插件中的Class必須保持包名和類名的唯一。否則,加載過的類不會再次被加載。
優缺點:各個Bundle之間完全可以相互調用,但是這也造成了各個Bundle之間ClassLoader的非隔離性。并且隨著數組的加長,每次findClass的時間會變長,對性能照成一定長度的影響。
讓我們在熟悉一下這張圖:
multidex

在DynamicAPK框架中,每個Bundle被加載到ClassLoader的調用棧如下:
Bundle的Application:BundleBaseApplication
->BundleBaseApplication(onCreate)
->BundleCore(run)
->BundleImpl(optDexFile)
->BundleArchiveRevision(optDexFile)
->BundlePathLoader(installBundleDexs)
->…

如下圖所示:
class

3.熱修復

由于所有的插件都被加載到同一個ClassLoader當中,因為,熱修復的方案都是從dexElements數組的順序入手,修改expandFieldArray方法的實現,將修復的類放到dexElements的前方。核心代碼如下(詳見BundlePathLoader):

 private static void expandFieldArray(Object instance, String fieldName,
                                         Object[] extraElements,boolean isHotFix) throws NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException {
        synchronized (BundlePathLoader.class) {
            Field jlrField = findField(instance, fieldName);
            Object[] original = (Object[]) jlrField.get(instance);
            Object[] combined = (Object[]) Array.newInstance(
                    original.getClass().getComponentType(), original.length + extraElements.length);
            if(isHotFix) {
                System.arraycopy(extraElements, 0, combined, 0, extraElements.length);
                System.arraycopy(original, 0, combined, extraElements.length, original.length);
            }else {
                System.arraycopy(original, 0, combined, 0, original.length);
                System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
            }
            jlrField.set(instance, combined);
        }
    }

調用的關鍵代碼如下(HotPatchItem.class):

   public void optDexFile() throws Exception{
        List<File> files = new ArrayList<File>();
        files.add(this.hotFixFile);
        BundlePathLoader.installBundleDexs(RuntimeArgs.androidApplication.getClassLoader(), storageDir, files, false);
    }

    public void optHotFixDexFile() throws Exception{
        List<File> files = new ArrayList<File>();
        files.add(this.hotFixFile);
        BundlePathLoader.installBundleDexs(RuntimeArgs.androidApplication.getClassLoader(), storageDir, files, true);
    }

4.運行時資源的加載

所有插件的資源都加載到DelegateResources中,關鍵代碼如下:
DelegateResources.class

...
public static void newDelegateResources(Application application, Resources resources) throws Exception {
        List<Bundle> bundles = Framework.getBundles();
        if (bundles != null && !bundles.isEmpty()) {
            Resources delegateResources;
            List<String> arrayList = new ArrayList();
            arrayList.add(application.getApplicationInfo().sourceDir);
            for (Bundle bundle : bundles) {
                arrayList.add(((BundleImpl) bundle).getArchive().getArchiveFile().getAbsolutePath());
            }
            AssetManager assetManager = AssetManager.class.newInstance();
            for (String str : arrayList) {
                SysHacks.AssetManager_addAssetPath.invoke(assetManager, str);
            }
            //處理小米UI資源
            if (resources == null || !resources.getClass().getName().equals("android.content.res.MiuiResources")) {
                delegateResources = new DelegateResources(assetManager, resources);
            } else {
                Constructor declaredConstructor = Class.forName("android.content.res.MiuiResources").getDeclaredConstructor(new Class[]{AssetManager.class, DisplayMetrics.class, Configuration.class});
                declaredConstructor.setAccessible(true);
                delegateResources = (Resources) declaredConstructor.newInstance(new Object[]{assetManager, resources.getDisplayMetrics(), resources.getConfiguration()});
            }
            RuntimeArgs.delegateResources = delegateResources;
            AndroidHack.injectResources(application, delegateResources);
            StringBuffer stringBuffer = new StringBuffer();
            stringBuffer.append("newDelegateResources [");
            for (int i = 0; i < arrayList.size(); i++) {
                if (i > 0) {
                    stringBuffer.append(",");
                }
                stringBuffer.append(arrayList.get(i));
            }
            stringBuffer.append("]");
            log.log(stringBuffer.toString(), Logger.LogLevel.DBUG);
        }
    }

...


上述代碼就是將所有Bundle中的資源,通過調用AssetManager的addAssetPath方法,加載到assetManager對象中,然后再用assetManager對象,創建delegateResources對象,并保存在RuntimeArgs.delegateResources當中,然后調用AndroidHack.injectResources方法,對Application和LoadedApk中的mResources成員變量進行注入,代碼如下:

 public static void injectResources(Application application, Resources resources) throws Exception {
        Object activityThread = getActivityThread();
        if (activityThread == null) {
            throw new Exception("Failed to get ActivityThread.sCurrentActivityThread");
        }
        Object loadedApk = getLoadedApk(activityThread, application.getPackageName());
        if (loadedApk == null) {
            throw new Exception("Failed to get ActivityThread.mLoadedApk");
        }
        SysHacks.LoadedApk_mResources.set(loadedApk, resources);
        SysHacks.ContextImpl_mResources.set(application.getBaseContext(), resources);
        SysHacks.ContextImpl_mTheme.set(application.getBaseContext(), null);
    }

其中,上述獲取LoadedApk的代碼,也是通過反射,獲取運行時ActivityThread類的LoadedApk對象.

5.運行時動態替換Resource對象

ContextImplHook,動態替換getResources

為了控制startActivity的時候,能夠及時替換Activity的Resource和AssetsManager對象,使用ContextImplHook類對Comtext進行替換,然后動態的返回上一步加載的RuntimeArgs.delegateResources委托資源對象。ContextImplHook的核心代碼如下:

    @Override
    public Resources getResources() {
        log.log("getResources is invoke", Logger.LogLevel.INFO);
        return RuntimeArgs.delegateResources;
    }

    @Override
    public AssetManager getAssets() {
        log.log("getAssets is invoke", Logger.LogLevel.INFO);
        return RuntimeArgs.delegateResources.getAssets();
    }

如何在Activity跳轉過程中,動態的替換呢

通過反射替換ActivityThread的mInstrumentation對象,替換成InstrumentationHook.class,然后就可以在執行startActivity時,攔截其newActivity和callActivityOnCreate方法,在newActivity方法中,動態的替換newActivity的mResources對象。在callActivityOnCreate方法中將ContextImplHook注入到新創建的Activity中。核心代碼如下:

    @Override
    public Activity newActivity(Class<?> cls, Context context, IBinder iBinder, Application application, Intent intent, ActivityInfo activityInfo, CharSequence charSequence, Activity activity, String str, Object obj) throws InstantiationException, IllegalAccessException {
        Activity newActivity = this.mBase.newActivity(cls, context, iBinder, application, intent, activityInfo, charSequence, activity, str, obj);
        if (RuntimeArgs.androidApplication.getPackageName().equals(activityInfo.packageName) && SysHacks.ContextThemeWrapper_mResources != null) {
            SysHacks.ContextThemeWrapper_mResources.set(newActivity, RuntimeArgs.delegateResources);
        }
        return newActivity;
    }

    @Override
    public Activity newActivity(ClassLoader classLoader, String str, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        Activity newActivity;
        try {
            newActivity = this.mBase.newActivity(classLoader, str, intent);
            if (SysHacks.ContextThemeWrapper_mResources != null) {
                SysHacks.ContextThemeWrapper_mResources.set(newActivity, RuntimeArgs.delegateResources);
            }
        } catch (ClassNotFoundException e) {
            String property = Framework.getProperty("ctrip.android.bundle.welcome", "ctrip.android.view.home.CtripSplashActivity");
            if (StringUtil.isEmpty(property)) {
                throw e;
            } else {
                List runningTasks = ((ActivityManager) this.context.getSystemService(Context.ACTIVITY_SERVICE)).getRunningTasks(1);
                if (runningTasks != null && runningTasks.size() > 0 && ((ActivityManager.RunningTaskInfo) runningTasks.get(0)).numActivities > 1) {
                    if (intent.getComponent() == null) {
                        intent.setClassName(this.context, str);
                    }
                }
                log.log("Could not find activity class: " + str, Logger.LogLevel.WARN);
                log.log("Redirect to welcome activity: " + property, Logger.LogLevel.WARN);
                newActivity = this.mBase.newActivity(classLoader, property, intent);
            }
        }
        return newActivity;
    }

    @Override
    public void callActivityOnCreate(Activity activity, Bundle bundle) {
        if (RuntimeArgs.androidApplication.getPackageName().equals(activity.getPackageName())) {
            ContextImplHook contextImplHook = new ContextImplHook(activity.getBaseContext());
            if (!(SysHacks.ContextThemeWrapper_mBase == null || SysHacks.ContextThemeWrapper_mBase.getField() == null)) {
                SysHacks.ContextThemeWrapper_mBase.set(activity, contextImplHook);
            }
            SysHacks.ContextWrapper_mBase.set(activity, contextImplHook);
        }
        this.mBase.callActivityOnCreate(activity, bundle);
    }

總結如下圖,Resource的加載和動態替換:
resource_replace

6.插件Activity在宿主AndroidManifest中的預注冊

每個插件的Activity,必須在宿主的AndroidManifest.xml中進行注冊。

DynamicAPK源碼導讀:

源代碼的目錄結構圖
sources

  • framework
    管理各個Bundle以及各個Bundle的封裝、版本控制等。
  • hack
    通過反射的形式,hack類,方法,成員變量等
  • hotpatch
    熱修復相關的封裝
  • loader
    對MultiDex的修改,各Bundle加載到ClassLoader,熱修復。
  • log
    日志管理
  • runtime
    運行時,對Resources進行動態替換
  • util
    工具類

博客相關Demo的Github地址

參考

1.AssetManager源碼
2.LoadedApk源碼
3.ActivityThread源碼
4.DynamicAPK源碼

來自: http://blog.csdn.net/nupt123456789/article/details/50531709

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!