從Instant run談Android替換Application和動態加載機制

轉自http://www.tuicool.com/articles/ZFbaaub

Android studio 2.0 Stable 版本中集成了 Install run 即時編譯技術,官方描述可以大幅加速編譯速度,我們團隊在第一時間更新並使用,總體用下來感覺,恩…也就那樣吧,還不如不用的快。所以就去看了下 Install run 的實現方式,其中有一個整體框架的基礎,也就是今天的文章的主題,Android替換Application和動態加載機制。

Instant run

Instant run 的大概實現原理可以看下這篇 Instant Run 淺析 ,我們需要知道 Instant run 使用的 gradle plugin2.0.0 ,源碼在 這裏 ,文中大概講了下 Instant run 的實現原理,但是並沒有深入細節,特別是替換Application和動態加載機制。

關於動態加載,實際上 Instant run 提供了兩種動態加載的機制:

1.修改java代碼需要重啓應用加載補丁dex,而在Application初始化時替換了Application,新建了一個自定義的ClassLoader去加載所有的dex文件。我們稱爲 重啓更新機制

2.修改代碼不需要重啓,新建一個 ClassLoader 去加載修改部分。我們稱爲 熱更新機制

Application入口

在編譯時 Instant run 用到了 Transform API 修改字節碼文件。其中 AndroidManifest.xml 文件也被修改,如下:

/app/build/intermediates/bundles/production/instant-run/AndroidManifest.xml ,其中的 Application 標籤

<application
        name="com.aa.bb.MyApplication"
        android:name="com.android.tools.fd.runtime.BootstrapApplication"
		... />

多了一個 com.android.tools.fd.runtime.BootstrapApplication ,在剛剛提到的gradle plugin 中的 instant-run-server 目錄下找到該文件。

實際上 BootstrapApplication 是我們app的實際入口,我們自己的 Application MyApplication 採用反射機制調用。

我們知道 Application  ContextWrapper 的子類

// android.app.Application
public class Application extends ContextWrapper {
    // ...
    public application() {
        super(null);
    }
    // ...
}
// android.content.ContextWrapper
public class ContextWrapper extends Context {
    Context mBase;
    // ...
    public ContextWrapper(Context base) {
        mBase = base;
    }
    protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }
    // ...
    @Override
    public AssetManager getAssets() {
        return mBase.getAssets();
    }
    @Override
    public Resources getResources()
    {
        return mBase.getResources();
    }
    // ...
}

ContextWrapper一方面繼承了Context,一方面又包含(composite)了一個Context對象(稱爲mBase),對Context的實現爲轉發給mBase對象處理。上面的代碼表示,在attachBaseContext 方式調用之前Application是沒有用的,因爲mBase是空的。所以我們看下 BootstrapApplication  attachBaseContext 方法

protected void attachBaseContext(Context context) {
        if (!AppInfo.usingApkSplits) {
            createResources(apkModified);
            //新建一個ClassLoader並設置爲原ClassLoader的parent
            setupClassLoaders(context, context.getCacheDir().getPath(), apkModified);
        }
		//通過Manifest中我們的實際Application即MyApplication名反射生成對象
        createRealApplication();
		//調用attachBaseContext完成初始化
        super.attachBaseContext(context);

        if (realApplication != null) {
        //反射調用實際Application的attachBaseContext方法
            try {
                Method attachBaseContext =
                        ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class);
                attachBaseContext.setAccessible(true);
                attachBaseContext.invoke(realApplication, context);
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        }
    }

初始化ClassLoader

//BootstrapApplication.setupClassLoaders
private static void setupClassLoaders(Context context, String codeCacheDir, long apkModified) {
		// /data/data/package_name/files/instant-run/dex/目錄下的dex列表
        List<String> dexList = FileManager.getDexList(context, apkModified);
            ClassLoader classLoader = BootstrapApplication.class.getClassLoader();
            String nativeLibraryPath = (String) classLoader.getClass().getMethod("getLdLibraryPath")
                                .invoke(classLoader);
            IncrementalClassLoader.inject(
                    classLoader,
                    nativeLibraryPath,
                    codeCacheDir,
                    dexList);
        }
    }
    
//IncrementalClassLoader.inject
public static ClassLoader inject(
            ClassLoader classLoader, String nativeLibraryPath, String codeCacheDir,
            List<String> dexes) {
        //新建一個自定義ClassLoader,dexPath爲參數中的dexList
        IncrementalClassLoader incrementalClassLoader =
                new IncrementalClassLoader(classLoader, nativeLibraryPath, codeCacheDir, dexes);
        //設置爲原ClassLoader的parent
        setParent(classLoader, incrementalClassLoader);
		return incrementalClassLoader;
    }

動態加載

新建一個自定義的 ClassLoader 名爲IncrementalClassLoader,該 ClassLoader 很簡單,就是 BaseDexClassLoader 的一個子類,並且將 IncrementalClassLoader 設置爲原ClassLoader的parent,熟悉JVM加載機制的同學應該都知道,由於ClassLoader採用雙親委託模式,即委託父類加載類,父類找不到再自己去找。這樣 IncrementalClassLoader 就變成了整個App的所有類的加載的ClassLoader,並且dexPath是 /data/data/package_name/files/instant-run/dex 目錄下的dex列表,這意味着什麼呢?


//``BaseDexClassLoader``的``findClass``
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c == null) {
        ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
        for (Throwable t : suppressedExceptions) {
            cnfe.addSuppressed(t);
        }
        throw cnfe;
    }
    return c;
}

可以看到,查找Class的任務通過pathList完成;這個pathList是一個DexPathList類的對象,它的findClass方法如下:

public Class findClass(String name, List<Throwable> suppressed) {
   for (Element element : dexElements) {
       DexFile dex = element.dexFile;

       if (dex != null) {
           Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
           if (clazz != null) {
               return clazz;
           }
       }
   }
   if (dexElementsSuppressedExceptions != null) {
       suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
   }
   return null;
}

這個DexPathList內部有一個叫做dexElements的數組,然後findClass的時候會遍歷這個數組來查找Class。看到了嗎,這個dexElements就是從dexPath來的,也就說是 IncrementalClassLoader 用來加載dexPath(/data/data/package_name/files/instant-run/dex/)下面的dex文件。感興趣的同學可以看下,我們app中的所有第三方庫和自己項目中的代碼,都被打包成若干個slice dex分片,該目錄下有幾十個dex文件。每當修改代碼用 Instant run 完成編譯,該目錄下的dex文件就會有一個或者幾個的更新時間發生改變。

正常情況下,apk被安裝之後,APK文件的代碼以及資源會被系統存放在固定的目錄(比如/data/app/package_name/base-1.apk )系統在進行類加載的時候,會自動去這一個或者幾個特定的路徑來尋找這個類。而使用 Install run 則完全不管之前的加載路徑,所有的分片dex文件和資源都在dexPath下,用 IncrementalClassLoader 去加載。也就是加載不存在APK固定路徑之外的類,即動態加載。

但是僅僅有ClassLoader是不夠的。因爲每個被修改的類都被改了名字,類名在原名後面添加 $override ,目錄在 app/build/intermediates/transforms/instantRun/debug/folders/4000 。AndroidManifest中並沒有註冊這些被改了名字的Activity。> 因此正常情況下系統無法加載我們插件中的類;因此也沒有辦法創建Activity的對象。

解決這個問題有兩個思路,要麼全盤接管這個類加載的過程;要麼告知系統我們使用的插件存在於哪裏,讓系統幫忙加載;這兩種方式或多或少都需要干預這個類加載的過程。

引用自 – Android 插件化原理解析——插件加載機制

動態加載的兩種方案

先來看下系統如何完成類的加載過程。

Activity 的創建過程

java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);

通過 ClassLoader 和類名加載,反射調用生成 Activity 對象,其中的 ClassLoader  LoadedApk 的一個對象 r.packageInfo 中獲得的。 LoadedApk 對象是APK文件在內存中的表示。 Apk文件的相關信息,諸如Apk文件的代碼和資源,甚至代碼裏面的 Activity  Service 等組件的信息我們都可以通過此對象獲取。

r.packageInfo的來源:

private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
        ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
        boolean registerPackage) {
        // 獲取userid信息
    final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
    synchronized (mResourcesManager) {
    // 嘗試獲取緩存信息
        WeakReference<LoadedApk> ref;
        if (differentUser) {
            // Caching not supported across users
            ref = null;
        } else if (includeCode) {
            ref = mPackages.get(aInfo.packageName);
        } else {
            ref = mResourcePackages.get(aInfo.packageName);
        }

        LoadedApk packageInfo = ref != null ? ref.get() : null;
        if (packageInfo == null || (packageInfo.mResources != null
                && !packageInfo.mResources.getAssets().isUpToDate())) {
                // 緩存沒有命中,直接new
            packageInfo =
                new LoadedApk(this, aInfo, compatInfo, baseLoader,
                        securityViolation, includeCode &&
                        (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);

        // 省略。。更新緩存
        return packageInfo;
    }
}

重要的是這個緩存 mPackage  LoadedApk 對象 packageInfo 就是從這個緩存中取的,所以我們只要在 mPackage 修改裏面的 ClassLoader 控制類的加載就能完成動態加載。

在《 Android 插件化原理解析——插件加載機制 》一文中,作者已經提出兩種動態加載的解決方案:

『激進方案』中我們自定義了插件的ClassLoader,並且繞開了Framework的檢測;利用ActivityThread對於LoadedApk的緩存機制,我們把攜帶這個自定義的ClassLoader的插件信息添加進mPackages中,進而完成了類的加載過程。

『保守方案』中我們深入探究了系統使用ClassLoader findClass的過程,發現應用程序使用的非系統類都是通過同一個PathClassLoader加載的;而這個類的最終父類BaseDexClassLoader通過DexPathList完成類的查找過程;我們hack了這個查找過程,從而完成了插件類的加載。

激進方案由於是一個插件一個 Classloader 也叫多 ClassLoader 方案,代表作 DroidPlugin ;保守方案也叫做單 ClassLoader 方案,代表作,Small、衆多熱更新框架如 nuwa 等。

Instant run的重啓更新機制

繞了一大圈,終於能接着往下看了。接上面,我們繼續看 BootstrapApplication onCreate 方法

public void onCreate() {
        MonkeyPatcher.monkeyPatchApplication(
                    BootstrapApplication.this, BootstrapApplication.this,
                    realApplication, externalResourcePath);
            MonkeyPatcher.monkeyPatchExistingResources(BootstrapApplication.this,
                    externalResourcePath, null);
        super.onCreate();
        ...
		//手機客戶端app和Android Studio建立Socket通信,AS是客戶端發消息,app		//是服務端接收消息作出相應操作。Instant run的通信方式。不在本文範圍內
        Server.create(AppInfo.applicationId, BootstrapApplication.this);

        if (realApplication != null) {
        	//還記得這個realApplication嗎,我們app中實際的Application
            realApplication.onCreate();
        }
    }

上面代碼,手機客戶端app和Android Studio建立Socket通信,AS是客戶端發消息,app是服務端接收消息作出相應操作,這是Instant run的通信方式,不在本文範圍內。然後反射調用實際 Application  onCreate 方法。

那麼前面的兩個 MonkeyPatcher 的方法是幹嘛的呢

先看 MonkeyPatcher.monkeyPatchApplication

public static void monkeyPatchApplication(@Nullable Context context,
                                              @Nullable Application bootstrap,
                                              @Nullable Application realApplication,
                                              @Nullable String externalResourceFile) {
        try {
            // Find the ActivityThread instance for the current thread
            Class<?> activityThread = Class.forName("android.app.ActivityThread");
            Object currentActivityThread = getActivityThread(context, activityThread);

            // Find the mInitialApplication field of the ActivityThread to the real application
            Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication");
            mInitialApplication.setAccessible(true);
            Application initialApplication = (Application) mInitialApplication.get(currentActivityThread);
            if (realApplication != null && initialApplication == bootstrap) {
            //**2.替換掉ActivityThread.mInitialApplication**
                mInitialApplication.set(currentActivityThread, realApplication);
            }

            // Replace all instance of the stub application in ActivityThread#mAllApplications with the
            // real one
            if (realApplication != null) {
                Field mAllApplications = activityThread.getDeclaredField("mAllApplications");
                mAllApplications.setAccessible(true);
                List<Application> allApplications = (List<Application>) mAllApplications
                        .get(currentActivityThread);
                for (int i = 0; i < allApplications.size(); i++) {
                    if (allApplications.get(i) == bootstrap) {
                    //**1.替換掉ActivityThread.mAllApplications**
                        allApplications.set(i, realApplication);
                    }
                }
            }

            // Figure out how loaded APKs are stored.

            // API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know.
            Class<?> loadedApkClass;
            try {
                loadedApkClass = Class.forName("android.app.LoadedApk");
            } catch (ClassNotFoundException e) {
                loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");
            }
            Field mApplication = loadedApkClass.getDeclaredField("mApplication");
            mApplication.setAccessible(true);
            Field mResDir = loadedApkClass.getDeclaredField("mResDir");
            mResDir.setAccessible(true);

            // 10 doesn't have this field, 14 does. Fortunately, there are not many Honeycomb devices
            // floating around.
            Field mLoadedApk = null;
            try {
                mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
            } catch (NoSuchFieldException e) {
                // According to testing, it's okay to ignore this.
            }

            // Enumerate all LoadedApk (or PackageInfo) fields in ActivityThread#mPackages and
            // ActivityThread#mResourcePackages and do two things:
            //   - Replace the Application instance in its mApplication field with the real one
            //   - Replace mResDir to point to the external resource file instead of the .apk. This is
            //     used as the asset path for new Resources objects.
            //   - Set Application#mLoadedApk to the found LoadedApk instance
            for (String fieldName : new String[]{"mPackages", "mResourcePackages"}) {
                Field field = activityThread.getDeclaredField(fieldName);
                field.setAccessible(true);
                Object value = field.get(currentActivityThread);

                for (Map.Entry<String, WeakReference<?>> entry :
                        ((Map<String, WeakReference<?>>) value).entrySet()) {
                    Object loadedApk = entry.getValue().get();
                    if (loadedApk == null) {
                        continue;
                    }

                    if (mApplication.get(loadedApk) == bootstrap) {
                        if (realApplication != null) {
                        //**3.替換掉mApplication**
                            mApplication.set(loadedApk, realApplication);
                        }
                        if (externalResourceFile != null) {
                        //替換掉資源目錄
                            mResDir.set(loadedApk, externalResourceFile);
                        }

                        if (realApplication != null && mLoadedApk != null) {
                        //**4.替換掉mLoadedApk**
                            mLoadedApk.set(realApplication, loadedApk);
                        }
                    }
                }
            }
        } catch (Throwable e) {
            throw new IllegalStateException(e);
        }
    }

這裏做了三件事情:

1.替換Application對象

BootstrapApplication 的作用就是加載 realApplication 也就是 MyApplication,所以我們就要把所有Framework層的 BootstrapApplication 對象替換爲 MyApplication 對象。包括:

baseContext.mPackageInfo.mApplication 代碼3
baseContext.mPackageInfo.mActivityThread.mInitialApplication 代碼2
baseContext.mPackageInfo.mActivityThread.mAllApplications 代碼1

2.替換資源相關對象mResDir,前面我們已經說過,正常情況下尋找資源都是在 /data/app/package_name/base-1.apk 目錄下,而 Instant run 將資源也抽出來放在 /data/data/package_name/files/instant-run/ ,加載目錄也更改爲後者

3.替換 mLoadedApk 對象

還記得前面的講的 LoadedApk 嗎,這裏面有加載類的 ClassLoader ,由於 BootstrapApplication  attachBaseContext 方法中就將其已經替換爲了 IncrementalClassLoader ,所以代碼4處反射將 BootstrapApplication  mLoadedApk 賦值給了 MyApplication ,那麼接下來MyApplication的所有類的加載都將由 IncrementalClassLoader 來負責。

MonkeyPatcher.monkeyPatchExistingResources 更新資源補丁,不在本文範圍內就不講了。

這些工作做完之後調用 MyApplication  onCreate 方法 BootstrapApplication就將控制權交給了 MyApplication ,這樣在整個運行環境中, MyApplication 就是正牌 Application 了,完成 Application 的替換。

總結一下,剛纔我們說了已經有兩個動態加載的方案,激進方案和保守方案,而 Instant run 的重啓更新機制更像後者–保守方案即單 ClassLoader 方案,首先,該種方案只有一個 ClassLoader ,只不過是通過替換 Application 達到的替換 mLoadedApk進而替換 ClassLoader 的目的,並沒有涉及到緩存 mPackage 然後dexList也是它自己維護的。

Instant run 熱更新機制

Instant run哪裏用到的熱更新機制呢?還記得剛纔我們提到的Socket通信嗎,其中S端也就是手機客戶端,接收到熱更新的消息會執行下面的方法:

private int handleHotSwapPatch(int updateMode, @NonNull ApplicationPatch patch) {
       try {
           String dexFile = FileManager.writeTempDexFile(patch.getBytes());
           String nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath();
           //新建一個ClassLoader,dexFile是剛更新的插件
           DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
                   mApplication.getCacheDir().getPath(), nativeLibraryPath,
                   getClass().getClassLoader());

           // we should transform this process with an interface/impl
           Class<?> aClass = Class.forName(
                   "com.android.tools.fd.runtime.AppPatchesLoaderImpl", true, dexClassLoader);
           try {
               PatchesLoader loader = (PatchesLoader) aClass.newInstance();
               String[] getPatchedClasses = (String[]) aClass
                       .getDeclaredMethod("getPatchedClasses").invoke(loader);
               //loader是PatchesLoader的一個實例,調用load方法加載插件
               if (!loader.load()) {
                   updateMode = UPDATE_MODE_COLD_SWAP;
               }
           } catch (Exception e) {
               updateMode = UPDATE_MODE_COLD_SWAP;
           }
       } catch (Throwable e) {
           updateMode = UPDATE_MODE_COLD_SWAP;
       }
       return updateMode;
   }

可以看到根據單個dexFile新建了一個 ClassLoader ,然後調用 loader.load() 方法, loader  PatchesLoader 接口的實例, PatchesLoader 接口的一個實現類 AppPatchesLoaderImpl ,該類中記錄了哪些修改的類。看一下 load 方法

@Override
    public boolean load() {
        try {
        //遍歷已記錄的所有修改的類
            for (String className : getPatchedClasses()) {
                ClassLoader cl = getClass().getClassLoader();
                //我們剛纔說的修改的類名後面都有$override
                Class<?> aClass = cl.loadClass(className + "$override");
                Object o = aClass.newInstance();
                //1.**反射修改原類中的$change字段爲修改後的值**
                Class<?> originalClass = cl.loadClass(className);
                Field changeField = originalClass.getDeclaredField("$change");
                // force the field accessibility as the class might not be "visible"
                // from this package.
                changeField.setAccessible(true);
                // If there was a previous change set, mark it as obsolete:
                Object previous = changeField.get(null);
                if (previous != null) {
                    Field isObsolete = previous.getClass().getDeclaredField("$obsolete");
                    if (isObsolete != null) {
                        isObsolete.set(null, true);
                    }
                }
                changeField.set(null, o);
            }
        } catch (Exception e) {
            return false;
        }
        return true;
    }

Instant run 的熱更新原理可以概述爲:

1.第一次運行,應用 transform API 修改字節碼。

輸出目錄在 app/build/intermediates/transforms/instantRun/debug/folders/1/,給所有的類添加 $change 字段, $change  IncrementalChange 類型, IncrementalChange 是個接口。如果 $change 不爲空,去調用 $change  access$dispatch方法,參數爲方法簽名字符串和方法參數數組,否則調用原邏輯。

load方法中會去加載全部補丁類,並賦值給對應原類的 $change 

這也驗證了我們說它是多 ClassLoader 方案。

2.所有修改的類有 gradle plugin 自動生成,類名在原名後面添加$override,複製修改後類的大部分方法,實現IncrementalChange 接口的access$dispatch方法,該方法會根據傳遞過來的方法簽名,調用本類的同名方法。

那麼也就是說只要把原類的 $change 字段設置爲該類,那就會調用該類的 access$dispatch 方法,就會使用修改後的方法了。上面代碼1處就通過反射修改了原類中的 $change 爲修改後補丁類中的值。 AppPatchesLoaderImpl 記錄了所有被修改的類,也會被打進補丁dex。

總結一下,可以看到 Instant run 熱更新是多 ClassLoader 加載方案,每個插件dex都有一個 ClassLoader ,如果插件需要升級,直接重新創建一個自定的 ClassLoader 加載新的插件。但是目前來看, Instant run 修改java代碼大部分情況下都是重啓更新機制,可能熱更新機制還有bug。資源更新是熱更新,重啓對應Activity就可以。

總結

Instant run 看下來真的有好多東西,其中就以替換 Application 和動態加載尤爲重要,關於動態加載,完全可以根據 Instant run 的實現方式完成一個熱修復和重啓修復相結合的更新框架,用於線上bug的修復和功能更新,並且可以支持資源文件的更新,是無侵入性的更新框架,最重要的一點,這是官方支持的。但是,性能肯定會有所影響,實際開發中使用 Instant run 編譯其實還有很多的問題,而且app初始化時使用的很多反射,這也直接導致app的啓動速度降低好多。

另外一點關於Application的替換是基於 bazel (一種構建工具,類似於burk)中的 StubApplication

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章