1. 程式人生 > >從Instant run談Android替換Application和動態載入機制

從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的實際入口,我們自己的ApplicationMyApplication採用反射機制呼叫。

我們知道ApplicationContextWrapper的子類

// 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是空的。所以我們看下BootstrapApplicationattachBaseContext方法

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的物件。

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

動態載入的兩種方案

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

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物件,其中的ClassLoaderLoadedApk的一個物件r.packageInfo中獲得的。LoadedApk物件是APK檔案在記憶體中的表示。 Apk檔案的相關資訊,諸如Apk檔案的程式碼和資源,甚至程式碼裡面的ActivityService等元件的資訊我們都可以通過此物件獲取。

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;
    }
}

重要的是這個快取mPackageLoadedApk物件packageInfo就是從這個快取中取的,所以我們只要在mPackage修改裡面的ClassLoader控制類的載入就能完成動態載入。

『激進方案』中我們自定義了外掛的ClassLoader,並且繞開了Framework的檢測;利用ActivityThread對於LoadedApk的快取機制,我們把攜帶這個自定義的ClassLoader的外掛資訊新增進mPackages中,進而完成了類的載入過程。

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

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

Instant run的重啟更新機制

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

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的通訊方式,不在本文範圍內。然後反射呼叫實際ApplicationonCreate方法。

那麼前面的兩個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) {
thrownew 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,由於BootstrapApplicationattachBaseContext方法中就將其已經替換為了IncrementalClassLoader,所以程式碼4處反射將BootstrapApplicationmLoadedApk賦值給了MyApplication,那麼接下來MyApplication的所有類的載入都將由IncrementalClassLoader來負責。

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

這些工作做完之後呼叫MyApplication

相關推薦

Instant runAndroid替換Application動態載入機制

轉自http://www.tuicool.com/articles/ZFbaaub Android studio 2.0 Stable版本中集成了Install run即時編譯技術,官方描述可以大幅加速編譯速度,我們團隊在第一時間更新並使用,總體用下來感覺,恩…也就那樣吧

Android中SerializableParcelable使用區別

Android中序列化有兩種方式:Serializable以及Parcelable。其中Serializable是Java自帶的,而Parcelable是安卓專有的。 一、Serializable序列化 serializable使用比較簡單,只需要對某個類實現Serializable 介面即可。 Ser

窗體洩漏android:configChanges屬性

首先看log日誌:has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView{1f60f27e V.E..... R.....ID 0,0-388,240} that was orig

Android FrameWork框架它在android的四層架構起到的作用

這裡寫一些關於Android Framework比較重要的知識點,這些東西對於之後理解和研究Android Framework有很

Android apk動態載入機制的研究(二) 資源載入activity生命週期管理

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

Android程序優先順序垃圾回收機制

程序優先順序低到高: 空程序 後臺進程序 服務程序 可見程序 前臺程序 垃圾回收機制: 引用計數法(未採用),無法處理迴圈引用問題.egA引用B,B引用C,C引用A。 標註並清理回收法(mark and sweep GC) 程式不停的建立新的物件,直到記憶體耗盡。再建立新的物件時,系統暫停其他元件執

Android WebView用法WebView載入提升網頁速度

前言 WebView是Android裡的元件,下面將全面介紹WebView的常用用法。 1.簡介 WebView是一個基於webkit引擎、展現web頁面的控制元件。Android的Webview

android 靜態庫動態庫編譯

android 庫的編譯依賴於nkd-build,使用之前請先安裝NDK。 編譯依賴 Android.mk 和 Application.mk 兩個檔案。 Android.mk(同時編譯靜態庫和動態庫): LOCAL_PATH := $(call my-di

Android之Activity觸控事件傳輸機制介紹

8 Activity觸控事件傳輸機制介紹 當我們觸控式螢幕幕的時候,程式會收到對應的觸控事件,這個事件是在app端去讀取的嗎?肯定不是,如果app能讀取,那會亂套的,所以app不會有這個許可權,系統按鍵的讀取以及分發都是通過WindowManagerService來完成

android的PicassoGlide載入本地圖片的區別

最近專案中有用到Picasso和Glide來載入本地圖片,發現有些區別 圖片路徑: String framePicPath="/storage/sdcard1/Android/data/com.example.lshapp.shortvideodemo /c

android 靜態廣播動態廣播的區別用法

一、什麼是廣播 BroadcastReceiver是android 系統的四大元件之一,本質上就是一個全域性的監聽器,用於監聽系統全域性的廣播訊息,可以方便的實現系統中不同元件之間的通訊。 程式可以通過呼叫context的sendBroadcast()方法來啟動指定的Br

Android靜態註冊動態註冊廣播的區別

1)靜態註冊:在AndroidManifest.xml註冊,android不能自動銷燬廣播接收器,也就是說當應用程式關閉後,還是會接收廣播。 2)動態註冊:在程式碼中通過registerReceive

Java動態編譯動態載入詳解

一.動態編譯 在某些情況下,我們需要動態生成java程式碼,通過動態編譯,然後執行程式碼。JAVA API提供了相應的工具(JavaCompiler)來實現動態編譯。 //獲取JavaCompiler JavaCompiler compiler = ToolProvider.getSyste

靜態連結庫(LIB)動態連結庫(DLL),DLL的靜態載入動態載入,兩種LIB檔案。

靜態連結庫(LIB)和動態連結庫(DLL),DLL的靜態載入和動態載入,兩種LIB檔案。 一、 靜態連結庫(LIB,也簡稱“靜態庫”)與動態連結庫(DLL,也簡稱“動態庫”)的區別 靜態連結庫與動態連結庫都是共享程式碼的方式,如果採用靜態連結庫,則無論你願不願意,lib 中的指令都全部被直接包含在最

Java代理動態代理機制分析應用

一、概述        代理是一種常用的設計模式,其目的就是為其他物件提供一個代理以控制對某個物件的訪問。代理類負責為委託類預處理訊息,過濾訊息並轉發訊息,以及進行訊息被委託類執行後的後續處理。根據代理類的生成時間不同可以將代理分為靜態代理和動態代理兩

Android中的動態載入機制 Android中的動態載入機制

Android中的動態載入機制   http:

Java虛擬機器(一):Java編譯器載入機制

目錄 編譯時 載入 連線 初始化 類載入器 類的載入 參考: 什麼是Java虛擬機器 從Java虛擬機器所做的事情上去理解,可以分為兩個階段,編譯時和執行時。編譯時主要是一個由編譯器將原始碼譯為虛擬機器指令集的一個過程;而執行

探祕類載入載入機制

在面向物件程式設計實踐中,我們通過眾多的類來組織一個複雜的系統,這些類之間相互關聯、呼叫使他們的關係形成了一個複雜緊密的網路。當系統啟動時,出於效能、資源利用多方面的考慮,我們不可能要求 JVM 一次性將全部的類都載入完成,而是隻載入能夠支援系統順利啟動和執行的類和資源即可。那麼在系統執行過程中如果需要使用未

Java的靜態載入動態載入區別

一、首先是說java的靜態載入: 1.建立了幾個類,“老師”、“學生”、“職員”,每個人群有個屬性方法,程式碼如下: public class Student { public void belongNature() { Syst

JAVA類的靜態載入動態載入以及NoClassDefFoundErrorClassNotFoundException

我們都知道JAVA初始化一個類的時候可以用new 操作符來初始化,也可通過Class.forName的方式來得到一個Class型別的例項,然後通過這個Class型別的例項的newInstance來初始化.我們把前者叫做JAVA的靜態載入,把後者叫做動態載入.後者在很多框架中