Android studio 2.0 Stable 版本中集成了 Install run 即時編譯技術,官方描述能夠大幅加速編譯速度。看了下 Install run 的實現方式,其中有一個總體框架的基礎,即Android替換Application和動態加載機制。java
Instant run 的大概實現原理能夠看下這篇 Instant Run 淺析 ,咱們須要知道 Instant run 使用的 gradle plugin2.0.0 ,源碼在 這裏 ,文中大概講了下 Instant run 的實現原理,可是並無深刻細節,特別是替換Application和動態加載機制。android
關於動態加載,實際上 Instant run 提供了兩種動態加載的機制:bootstrap
在編譯時 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 採用反射機制調用。app
咱們知道 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 方法ide
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); } } }
初始化ClassLoadergradle
//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的對象。
解決這個問題有兩個思路,要麼全盤接管這個類加載的過程;要麼告知系統咱們使用的插件存在於哪裏,讓系統幫忙加載;這兩種方式或多或少都須要干預這個類加載的過程。
ref: 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 等。
繼續看 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哪裏用到的熱更新機制呢?還記得剛纔咱們提到的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的啓動速度下降好多。
從Instant run看Application替換和動態加載機制