深入理解 Android_Instant_Run 運行機制

308232176 7年前發布 | 10K 次閱讀 安卓開發 Android開發 移動開發

Instant Run

Instant Run,是android studio2.0新增的一個運行機制,在你編碼開發、測試或debug的時候,它都能顯著減少你對當前應用的構建和部署的時間。通俗的解釋就是,當你在Android Studio中改了你的代碼,Instant Run可以很快的讓你看到你修改的效果。而在沒有Instant Run之前,你的一個小小的修改,都肯能需要幾十秒甚至更長的等待才能看到修改后的效果。

傳統的代碼修改及編譯部署流程

傳統的代碼修改及編譯流程如下:構建整個apk → 部署app → app重啟 → 重啟Activity

Instant Run編譯和部署流程

Instant Run構建項目的流程:構建修改的部分 → 部署修改的dex或資源 → 熱部署,溫部署,冷部署

熱拔插,溫拔插,冷拔插

熱拔插:代碼改變被應用、投射到APP上,不需要重啟應用,不需要重建當前activity。

場景:適用于多數的簡單改變(包括一些方法實現的修改,或者變量值修改)

溫拔插:activity需要被重啟才能看到所需更改。

場景:典型的情況是代碼修改涉及到了資源文件,即resources。

冷拔插:app需要被重啟(但是仍然不需要重新安裝)

場景:任何涉及結構性變化的,比如:修改了繼承規則、修改了方法簽名等。

首次運行Instant Run,Gradle執行過程

一個新的App Server類會被注入到App中,與Bytecode instrumentation協同監控代碼的變化。

同時會有一個新的Application類,它注入了一個自定義類加載器(Class Loader),同時該Application類會啟動我們所需的新注入的App Server。于是,Manifest會被修改來確保我們的應用能使用這個新的Application類。(這里不必擔心自己繼承定義了Application類,Instant Run添加的這個新Application類會代理我們自定義的Application類)

至此,Instant Run已經可以跑起來了,在我們使用的時候,它會通過決策,合理運用冷溫熱拔插來協助我們大量地縮短構建程序的時間。

在Instant Run運行之前,Android Studio會檢查是否能連接到App Server中。并且確保這個App Server是Android Studio所需要的。這同樣能確保該應用正處在前臺。

熱拔插

Android Studio monitors: 運行著Gradle任務來生成增量.dex文件(這個dex文件是對應著開發中的修改類) Android Studio會提取這些.dex文件發送到App Server,然后部署到App。

App Server會不斷監聽是否需要重寫類文件,如果需要,任務會被立馬執行。新的更改便能立即被響應。我們可以通過打斷點的方式來查看。

溫拔插

溫拔插需要重啟Activity,因為資源文件是在Activity創建時加載,所以必須重啟Activity來重載資源文件。

目前來說,任何資源文件的修改都會導致重新打包再發送到APP。但是,google的開發團隊正在致力于開發一個增量包,這個增量包只會包裝修改過的資源文件并能部署到當前APP上。

所以溫拔插實際上只能應對少數的情況,它并不能應付應用在架構、結構上的變化。

注:溫拔插涉及到的資源文件修改,在manifest上是無效的(這里的無效是指不會啟動Instant Run),因為,manifest的值是在APK安裝的時候被讀取,所以想要manifest下資源的修改生效,還需要觸發一個完整的應用構建和部署。

冷拔插

應用部署的時候,會把工程拆分成十個部分,每部分都擁有自己的.dex文件,然后所有的類會根據包名被分配給相應的.dex文件。當冷拔插開啟時,修改過的類所對應的.dex文件,會重組生成新的.dex文件,然后再部署到設備上。

之所以能這么做,是依賴于Android的ART模式,它能允許加載多個.dex文件。ART模式在android4.4(API-19)中加入,但是Dalvik依然是首選,到了android5.0(API-21),ART模式才成為系統默認首選,所以Instant Run只能運行在API-21及其以上版本。

使用Instant Run一些注意點

Instant Run是被Android Studio控制的。所以我們只能通過IDE來啟動它,如果通過設備來啟動應用,Instant Run會出現異常情況。在使用Instant Run來啟動Android app的時候,應注意以下幾點:

  1. 如果應用的minSdkVersion小于21,可能多數的Instant Run功能會掛掉,這里提供一個解決方法,通過product flavor建立一個minSdkVersion大于21的新分支,用來debug。
  2. Instant Run目前只能在主進程里運行,如果應用是多進程的,類似微信,把webView抽出來單獨一個進程,那熱、溫拔插會被降級為冷拔插。
  3. 在Windows下,Windows Defender Real-Time Protection可能會導致Instant Run掛掉,可用通過添加白名單列表解決。
  4. 暫時不支持Jack compiler,Instrumentation Tests,或者同時部署到多臺設備。

結合Demo深度理解

為了方便大家的理解,我們新建一個項目,里面不寫任何的邏輯功能,只對application做一個修改:

首先,我們先反編譯一下APK的構成,使用的工具:d2j-dex2jar 和jd-gui。

我們要看的啟動的信息就在這個instant-run.zip文件里面,解壓instant-run.zip,我們會發現,我們真正的業務代碼都在這里。

從instant-run文件中我們猜想是BootstrapApplication替換了我們的application,Instant-Run代碼作為一個宿主程序,將app作為資源dex加載起來。

那么InstantRun是怎么把業務代碼運行起來的呢?

Instant Run如何啟動app

按照我們上面對instant-run運行機制的猜想,我們首先看一下appliaction的分析attachBaseContext和onCreate方法。

attachBaseContext()

protected void attachBaseContext(Context context) {
if (!AppInfo.usingApkSplits) {
String apkFile = context.getApplicationInfo().sourceDir;
long apkModified = apkFile != null ? new File(apkFile)
.lastModified() : 0L;
createResources(apkModified);
setupClassLoaders(context, context.getCacheDir().getPath(),
apkModified);
}
createRealApplication();
super.attachBaseContext(context);
if (this.realApplication != null) {
try {
Method attachBaseContext = ContextWrapper.class
.getDeclaredMethod("attachBaseContext",
new Class[] { Context.class });
attachBaseContext.setAccessible(true);
attachBaseContext.invoke(this.realApplication,
new Object[] { context });
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}

我們依次需要關注的方法有:

createResources → setupClassLoaders → createRealApplication → 調用realApplication的attachBaseContext方法

createResources()

private void createResources(long apkModified) {
FileManager.checkInbox();
File file = FileManager.getExternalResourceFile();
this.externalResourcePath = (file != null ? file.getPath() : null);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Resource override is "

  • this.externalResourcePath); } if (file != null) { try { long resourceModified = file.lastModified(); if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Resource patch last modified: "
  • resourceModified); Log.v("InstantRun", "APK last modified: " + apkModified
  • " "
  • (apkModified > resourceModified ? ">" : "<")
  • " resource patch"); } if ((apkModified == 0L) || (resourceModified <= apkModified)) { if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Ignoring resource file, older than APK"); } this.externalResourcePath = null; } } catch (Throwable t) { Log.e("InstantRun", "Failed to check patch timestamps", t); } } }</code></pre>

    說明:該方法主要是判斷資源resource.ap_是否改變,然后保存resource.ap_的路徑到externalResourcePath中。

    setupClassLoaders()

    private static void setupClassLoaders(Context context, String codeCacheDir,
    long apkModified) {
    List dexList = FileManager.getDexList(context, apkModified);
    Class server = Server.class;
    Class patcher = MonkeyPatcher.class;
    if (!dexList.isEmpty()) {
    if (Log.isLoggable("InstantRun", 2)) {
    Log.v("InstantRun", "Bootstrapping class loader with dex list "
  • join('\n', dexList)); } ClassLoader classLoader = BootstrapApplication.class .getClassLoader(); String nativeLibraryPath; try { nativeLibraryPath = (String) classLoader.getClass() .getMethod("getLdLibraryPath", new Class[0]) .invoke(classLoader, new Object[0]); if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Native library path: "
  • nativeLibraryPath); } } catch (Throwable t) { Log.e("InstantRun", "Failed to determine native library path "
  • t.getMessage()); nativeLibraryPath = FileManager.getNativeLibraryFolder() .getPath(); } IncrementalClassLoader.inject(classLoader, nativeLibraryPath, codeCacheDir, dexList); } }</code></pre>

    說明,該方法是初始化一個ClassLoaders并調用IncrementalClassLoader。

    IncrementalClassLoader的源碼如下:

    public class IncrementalClassLoader extends ClassLoader {
    public static final boolean DEBUG_CLASS_LOADING = false;
    private final DelegateClassLoader delegateClassLoader;
    public IncrementalClassLoader(ClassLoader original,
    String nativeLibraryPath, String codeCacheDir, List dexes) {
    super(original.getParent());
    this.delegateClassLoader = createDelegateClassLoader(nativeLibraryPath,
    codeCacheDir, dexes, original);
    }
    public Class findClass(String className) throws ClassNotFoundException {
    try {
    return this.delegateClassLoader.findClass(className);
    } catch (ClassNotFoundException e) {
    throw e;
    }
    }
    private static class DelegateClassLoader extends BaseDexClassLoader {
    private DelegateClassLoader(String dexPath, File optimizedDirectory,
    String libraryPath, ClassLoader parent) {
    super(dexPath, optimizedDirectory, libraryPath, parent);
    }
    public Class findClass(String name) throws ClassNotFoundException {
    try {
    return super.findClass(name);
    } catch (ClassNotFoundException e) {
    throw e;
    }
    }
    }
    private static DelegateClassLoader createDelegateClassLoader(
    String nativeLibraryPath, String codeCacheDir, List dexes,
    ClassLoader original) {
    String pathBuilder = createDexPath(dexes);
    return new DelegateClassLoader(pathBuilder, new File(codeCacheDir),
    nativeLibraryPath, original);
    }
    private static String createDexPath(List dexes) {
    StringBuilder pathBuilder = new StringBuilder();
    boolean first = true;
    for (String dex : dexes) {
    if (first) {
    first = false;
    } else {
    pathBuilder.append(File.pathSeparator);
    }
    pathBuilder.append(dex);
    }
    if (Log.isLoggable("InstantRun", 2)) {
    Log.v("InstantRun", "Incremental dex path is "
  • BootstrapApplication.join('\n', dexes)); } return pathBuilder.toString(); } private static void setParent(ClassLoader classLoader, ClassLoader newParent) { try { Field parent = ClassLoader.class.getDeclaredField("parent"); parent.setAccessible(true); parent.set(classLoader, newParent); } catch (IllegalArgumentException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } } public static ClassLoader inject(ClassLoader classLoader, String nativeLibraryPath, String codeCacheDir, List dexes) { IncrementalClassLoader incrementalClassLoader = new IncrementalClassLoader( classLoader, nativeLibraryPath, codeCacheDir, dexes); setParent(classLoader, incrementalClassLoader); return incrementalClassLoader; } }</code></pre>

    inject方法是用來設置classloader的父子順序的,使用IncrementalClassLoader來加載dex。由于ClassLoader的雙親委托模式,也就是委托父類加載類,父類中找不到再在本ClassLoader中查找。

    調用的效果圖如下:

    為了方便我們對委托父類加載機制的理解,我們可以做一個實驗,在我們的application做一些Log。

    @Override
    public void onCreate() {
    super.onCreate();
    try{
    Log.d(TAG,"###onCreate in myApplication");
    String classLoaderName = getClassLoader().getClass().getName();
    Log.d(TAG,"###onCreate in myApplication classLoaderName = "+classLoaderName);
    String parentClassLoaderName = getClassLoader().getParent().getClass().getName();
    Log.d(TAG,"###onCreate in myApplication parentClassLoaderName = "+parentClassLoaderName);
    String pParentClassLoaderName = getClassLoader().getParent().getParent().getClass().getName();
    Log.d(TAG,"###onCreate in myApplication pParentClassLoaderName = "+pParentClassLoaderName);
    }catch (Exception e){
    e.printStackTrace();
    }
    }

    輸出結果:

    03-20 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication classLoaderName = dalvik.system.PathClassLoader
    03-20 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication parentClassLoaderName = com.android.tools.fd.runtime.IncrementalClassLoader
    03-20 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication pParentClassLoaderName = java.lang.BootClassLoader

    由此,我們知道,當前PathClassLoader委托IncrementalClassLoader加載dex。

    我們繼續對attachBaseContext()繼續分析:

    attachBaseContext.invoke(this.realApplication,
    new Object[] { context });

    createRealApplication

    private void createRealApplication() {
    if (AppInfo.applicationClass != null) {
    if (Log.isLoggable("InstantRun", 2)) {
    Log.v("InstantRun",
    "About to create real application of class name = "
  • AppInfo.applicationClass); } try { Class realClass = (Class) Class .forName(AppInfo.applicationClass); if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Created delegate app class successfully : "
  • realClass + " with class loader "
  • realClass.getClassLoader()); } Constructor constructor = realClass .getConstructor(new Class[0]); this.realApplication = ((Application) constructor .newInstance(new Object[0])); if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Created real app instance successfully :"
  • this.realApplication); } } catch (Exception e) { throw new IllegalStateException(e); } } else { this.realApplication = new Application(); } }</code></pre>

    該方法就是用classes.dex中的AppInfo類的applicationClass常量中保存的app真實的application。由例子的分析我們可以知道applicationClass就是com.xzh.demo.MyApplication。通過反射的方式,創建真是的realApplication。

    看完attachBaseContext我們繼續看BootstrapApplication();

    BootstrapApplication()

    我們首先看一下onCreate方法:

    onCreate()

    public void onCreate() {
    if (!AppInfo.usingApkSplits) {
    MonkeyPatcher.monkeyPatchApplication(this, this,
    this.realApplication, this.externalResourcePath);
    MonkeyPatcher.monkeyPatchExistingResources(this,
    this.externalResourcePath, null);
    } else {
    MonkeyPatcher.monkeyPatchApplication(this, this,
    this.realApplication, null);
    }
    super.onCreate();
    if (AppInfo.applicationId != null) {
    try {
    boolean foundPackage = false;
    int pid = Process.myPid();
    ActivityManager manager = (ActivityManager) getSystemService("activity");
    List processes = manager
    .getRunningAppProcesses();
    boolean startServer = false;
    if ((processes != null) && (processes.size() > 1)) {
    for (ActivityManager.RunningAppProcessInfo processInfo : processes) {
    if (AppInfo.applicationId
    .equals(processInfo.processName)) {
    foundPackage = true;
    if (processInfo.pid == pid) {
    startServer = true;
    break;
    }
    }
    }
    if ((!startServer) && (!foundPackage)) {
    startServer = true;
    if (Log.isLoggable("InstantRun", 2)) {
    Log.v("InstantRun",
    "Multiprocess but didn't find process with package: starting server anyway");
    }
    }
    } else {
    startServer = true;
    }
    if (startServer) {
    Server.create(AppInfo.applicationId, this);
    }
    } catch (Throwable t) {
    if (Log.isLoggable("InstantRun", 2)) {
    Log.v("InstantRun", "Failed during multi process check", t);
    }
    Server.create(AppInfo.applicationId, this);
    }
    }
    if (this.realApplication != null) {
    this.realApplication.onCreate();
    }
    }

    在onCreate()中我們需要注意以下方法:

    monkeyPatchApplication → monkeyPatchExistingResources → Server啟動 → 調用realApplication的onCreate方法

    monkeyPatchApplication

    public static void monkeyPatchApplication(Context context,
    Application bootstrap, Application realApplication,
    String externalResourceFile) {
    try {
    Class activityThread = Class
    .forName("android.app.ActivityThread");
    Object currentActivityThread = getActivityThread(context,
    activityThread);
    Field mInitialApplication = activityThread
    .getDeclaredField("mInitialApplication");
    mInitialApplication.setAccessible(true);
    Application initialApplication = (Application) mInitialApplication
    .get(currentActivityThread);
    if ((realApplication != null) && (initialApplication == bootstrap)) {
    mInitialApplication.set(currentActivityThread, realApplication);
    }
    if (realApplication != null) {
    Field mAllApplications = activityThread
    .getDeclaredField("mAllApplications");
    mAllApplications.setAccessible(true);
    List allApplications = (List) mAllApplications
    .get(currentActivityThread);
    for (int i = 0; i < allApplications.size(); i++) {
    if (allApplications.get(i) == bootstrap) {
    allApplications.set(i, realApplication);
    }
    }
    }
    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);
    Field mLoadedApk = null;
    try {
    mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
    } catch (NoSuchFieldException e) {
    }
    for (String fieldName : new String[] { "mPackages",
    "mResourcePackages" }) {
    Field field = activityThread.getDeclaredField(fieldName);
    field.setAccessible(true);
    Object value = field.get(currentActivityThread);
    for (Map.Entry> entry : ((Map>) value)
    .entrySet()) {
    Object loadedApk = ((WeakReference) entry.getValue()).get();
    if (loadedApk != null) {
    if (mApplication.get(loadedApk) == bootstrap) {
    if (realApplication != null) {
    mApplication.set(loadedApk, realApplication);
    }
    if (externalResourceFile != null) {
    mResDir.set(loadedApk, externalResourceFile);
    }
    if ((realApplication != null)
    && (mLoadedApk != null)) {
    mLoadedApk.set(realApplication, loadedApk);
    }
    }
    }
    }
    }
    } catch (Throwable e) {
    throw new IllegalStateException(e);
    }
    }

    說明:該方法的作用是替換所有當前app的application為realApplication。

    替換的過程如下:

    1.替換ActivityThread的mInitialApplication為realApplication

    2.替換mAllApplications 中所有的Application為realApplication

    3.替換ActivityThread的mPackages,mResourcePackages中的mLoaderApk中的application為realApplication。

    monkeyPatchExistingResources

    public static void monkeyPatchExistingResources(Context context,
    String externalResourceFile, Collection activities) {
    if (externalResourceFile == null) {
    return;
    }
    try {
    AssetManager newAssetManager = (AssetManager) AssetManager.class
    .getConstructor(new Class[0]).newInstance(new Object[0]);
    Method mAddAssetPath = AssetManager.class.getDeclaredMethod(
    "addAssetPath", new Class[] { String.class });
    mAddAssetPath.setAccessible(true);
    if (((Integer) mAddAssetPath.invoke(newAssetManager,
    new Object[] { externalResourceFile })).intValue() == 0) {
    throw new IllegalStateException(
    "Could not create new AssetManager");
    }
    Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod(
    "ensureStringBlocks", new Class[0]);
    mEnsureStringBlocks.setAccessible(true);
    mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
    if (activities != null) {
    for (Activity activity : activities) {
    Resources resources = activity.getResources();
    try {
    Field mAssets = Resources.class
    .getDeclaredField("mAssets");
    mAssets.setAccessible(true);
    mAssets.set(resources, newAssetManager);
    } catch (Throwable ignore) {
    Field mResourcesImpl = Resources.class
    .getDeclaredField("mResourcesImpl");
    mResourcesImpl.setAccessible(true);
    Object resourceImpl = mResourcesImpl.get(resources);
    Field implAssets = resourceImpl.getClass()
    .getDeclaredField("mAssets");
    implAssets.setAccessible(true);
    implAssets.set(resourceImpl, newAssetManager);
    }
    Resources.Theme theme = activity.getTheme();
    try {
    try {
    Field ma = Resources.Theme.class
    .getDeclaredField("mAssets");
    ma.setAccessible(true);
    ma.set(theme, newAssetManager);
    } catch (NoSuchFieldException ignore) {
    Field themeField = Resources.Theme.class
    .getDeclaredField("mThemeImpl");
    themeField.setAccessible(true);
    Object impl = themeField.get(theme);
    Field ma = impl.getClass().getDeclaredField(
    "mAssets");
    ma.setAccessible(true);
    ma.set(impl, newAssetManager);
    }
    Field mt = ContextThemeWrapper.class
    .getDeclaredField("mTheme");
    mt.setAccessible(true);
    mt.set(activity, null);
    Method mtm = ContextThemeWrapper.class
    .getDeclaredMethod("initializeTheme",
    new Class[0]);
    mtm.setAccessible(true);
    mtm.invoke(activity, new Object[0]);
    Method mCreateTheme = AssetManager.class
    .getDeclaredMethod("createTheme", new Class[0]);
    mCreateTheme.setAccessible(true);
    Object internalTheme = mCreateTheme.invoke(
    newAssetManager, new Object[0]);
    Field mTheme = Resources.Theme.class
    .getDeclaredField("mTheme");
    mTheme.setAccessible(true);
    mTheme.set(theme, internalTheme);
    } catch (Throwable e) {
    Log.e("InstantRun",
    "Failed to update existing theme for activity "
  • activity, e); } pruneResourceCaches(resources); } } Collection> references; if (Build.VERSION.SDK_INT >= 19) { Class resourcesManagerClass = Class .forName("android.app.ResourcesManager"); Method mGetInstance = resourcesManagerClass.getDeclaredMethod( "getInstance", new Class[0]); mGetInstance.setAccessible(true); Object resourcesManager = mGetInstance.invoke(null, new Object[0]); try { Field fMActiveResources = resourcesManagerClass .getDeclaredField("mActiveResources"); fMActiveResources.setAccessible(true); ArrayMap> arrayMap = (ArrayMap) fMActiveResources .get(resourcesManager); references = arrayMap.values(); } catch (NoSuchFieldException ignore) { Field mResourceReferences = resourcesManagerClass .getDeclaredField("mResourceReferences"); mResourceReferences.setAccessible(true); references = (Collection) mResourceReferences .get(resourcesManager); } } else { Class activityThread = Class .forName("android.app.ActivityThread"); Field fMActiveResources = activityThread .getDeclaredField("mActiveResources"); fMActiveResources.setAccessible(true); Object thread = getActivityThread(context, activityThread); HashMap> map = (HashMap) fMActiveResources .get(thread); references = map.values(); } for (WeakReference wr : references) { Resources resources = (Resources) wr.get(); if (resources != null) { try { Field mAssets = Resources.class .getDeclaredField("mAssets"); mAssets.setAccessible(true); mAssets.set(resources, newAssetManager); } catch (Throwable ignore) { Field mResourcesImpl = Resources.class .getDeclaredField("mResourcesImpl"); mResourcesImpl.setAccessible(true); Object resourceImpl = mResourcesImpl.get(resources); Field implAssets = resourceImpl.getClass() .getDeclaredField("mAssets"); implAssets.setAccessible(true); implAssets.set(resourceImpl, newAssetManager); } resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics()); } } } catch (Throwable e) { throw new IllegalStateException(e); } }</code></pre>

    說明:該方法的作用是替換所有當前app的mAssets為newAssetManager。

    monkeyPatchExistingResources的流程如下:

    1.如果resource.ap_文件有改變,那么新建一個AssetManager對象newAssetManager,然后用newAssetManager對象替換所有當前Resource、Resource.Theme的mAssets成員變量。

    2.如果當前的已經有Activity啟動了,還需要替換所有Activity中mAssets成員變量

    判斷Server是否已經啟動,如果沒有啟動,則啟動Server。然后調用realApplication的onCreate方法代理realApplication的生命周期。

    接下來我們分析下Server負責的**熱部署**、**溫部署**和**冷部署**等問題。

    Server熱部署、溫部署和冷部署

    首先重點關注一下Server的內部類SocketServerReplyThread。

    SocketServerReplyThread

    private class SocketServerReplyThread extends Thread {
    private final LocalSocket mSocket;
    SocketServerReplyThread(LocalSocket socket) {
    this.mSocket = socket;
    }
    public void run() {
    try {
    DataInputStream input = new DataInputStream(
    this.mSocket.getInputStream());
    DataOutputStream output = new DataOutputStream(
    this.mSocket.getOutputStream());
    try {
    handle(input, output);
    } finally {
    try {
    input.close();
    } catch (IOException ignore) {
    }
    try {
    output.close();
    } catch (IOException ignore) {
    }
    }
    return;
    } catch (IOException e) {
    if (Log.isLoggable("InstantRun", 2)) {
    Log.v("InstantRun", "Fatal error receiving messages", e);
    }
    }
    }
    private void handle(DataInputStream input, DataOutputStream output)
    throws IOException {
    long magic = input.readLong();
    if (magic != 890269988L) {
    Log.w("InstantRun",
    "Unrecognized header format " + Long.toHexString(magic));
    return;
    }
    int version = input.readInt();
    output.writeInt(4);
    if (version != 4) {
    Log.w("InstantRun",
    "Mismatched protocol versions; app is using version 4 and tool is using version "
  • version); } else { int message; for (;;) { message = input.readInt(); switch (message) { case 7: if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Received EOF from the IDE"); } return; case 2: boolean active = Restarter .getForegroundActivity(Server.this.mApplication) != null; output.writeBoolean(active); if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Received Ping message from the IDE; returned active = "
  • active); } break; case 3: String path = input.readUTF(); long size = FileManager.getFileSize(path); output.writeLong(size); if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Received path-exists(" + path
  • ") from the " + "IDE; returned size="
  • size); } break; case 4: long begin = System.currentTimeMillis(); path = input.readUTF(); byte[] checksum = FileManager.getCheckSum(path); if (checksum != null) { output.writeInt(checksum.length); output.write(checksum); if (Log.isLoggable("InstantRun", 2)) { long end = System.currentTimeMillis(); String hash = new BigInteger(1, checksum) .toString(16); Log.v("InstantRun", "Received checksum(" + path
  • ") from the " + "IDE: took "
  • (end - begin) + "ms to compute "
  • hash); } } else { output.writeInt(0); if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Received checksum(" + path
  • ") from the "
  • "IDE: returning "); } } break; case 5: if (!authenticate(input)) { return; } Activity activity = Restarter .getForegroundActivity(Server.this.mApplication); if (activity != null) { if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Restarting activity per user request"); } Restarter.restartActivityOnUiThread(activity); } break; case 1: if (!authenticate(input)) { return; } List changes = ApplicationPatch .read(input); if (changes != null) { boolean hasResources = Server.hasResources(changes); int updateMode = input.readInt(); updateMode = Server.this.handlePatches(changes, hasResources, updateMode); boolean showToast = input.readBoolean(); output.writeBoolean(true); Server.this.restart(updateMode, hasResources, showToast); } break; case 6: String text = input.readUTF(); Activity foreground = Restarter .getForegroundActivity(Server.this.mApplication); if (foreground != null) { Restarter.showToast(foreground, text); } else if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Couldn't show toast (no activity) : "
  • text); } break; } } } } }</code></pre>

    說明:socket開啟后,開始讀取數據,當讀到1時,獲取代碼變化的ApplicationPatch列表,然后調用handlePatches來處理代碼的變化。

    handlePatches

    private int handlePatches(List changes,
    boolean hasResources, int updateMode) {
    if (hasResources) {
    FileManager.startUpdate();
    }
    for (ApplicationPatch change : changes) {
    String path = change.getPath();
    if (path.endsWith(".dex")) {
    handleColdSwapPatch(change);
    boolean canHotSwap = false;
    for (ApplicationPatch c : changes) {
    if (c.getPath().equals("classes.dex.3")) {
    canHotSwap = true;
    break;
    }
    }
    if (!canHotSwap) {
    updateMode = 3;
    }
    } else if (path.equals("classes.dex.3")) {
    updateMode = handleHotSwapPatch(updateMode, change);
    } else if (isResourcePath(path)) {
    updateMode = handleResourcePatch(updateMode, change, path);
    }
    }
    if (hasResources) {
    FileManager.finishUpdate(true);
    }
    return updateMode;
    }

    說明:本方法主要通過判斷Change的內容,來判斷采用什么模式(熱部署、溫部署或冷部署)

    • 如果后綴為“.dex”,冷部署處理handleColdSwapPatch
    • 如果后綴為“classes.dex.3”,熱部署處理handleHotSwapPatch
    • 其他情況,溫部署,處理資源handleResourcePatch

    handleColdSwapPatch冷部署

    private static void handleColdSwapPatch(ApplicationPatch patch) {
    if (patch.path.startsWith("slice-")) {
    File file = FileManager.writeDexShard(patch.getBytes(), patch.path);
    if (Log.isLoggable("InstantRun", 2)) {
    Log.v("InstantRun", "Received dex shard " + file);
    }
    }
    }

    說明:該方法把dex文件寫到私有目錄,等待整個app重啟,重啟之后,使用前面提到的IncrementalClassLoader加載dex即可。

    handleHotSwapPatch熱部署

    private int handleHotSwapPatch(int updateMode, ApplicationPatch patch) {
    if (Log.isLoggable("InstantRun", 2)) {
    Log.v("InstantRun", "Received incremental code patch");
    }
    try {
    String dexFile = FileManager.writeTempDexFile(patch.getBytes());
    if (dexFile == null) {
    Log.e("InstantRun", "No file to write the code to");
    return updateMode;
    }
    if (Log.isLoggable("InstantRun", 2)) {
    Log.v("InstantRun", "Reading live code from " + dexFile);
    }
    String nativeLibraryPath = FileManager.getNativeLibraryFolder()
    .getPath();
    DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
    this.mApplication.getCacheDir().getPath(),
    nativeLibraryPath, getClass().getClassLoader());
    Class aClass = Class.forName(
    "com.android.tools.fd.runtime.AppPatchesLoaderImpl", true,
    dexClassLoader);
    try {
    if (Log.isLoggable("InstantRun", 2)) {
    Log.v("InstantRun", "Got the patcher class " + aClass);
    }
    PatchesLoader loader = (PatchesLoader) aClass.newInstance();
    if (Log.isLoggable("InstantRun", 2)) {
    Log.v("InstantRun", "Got the patcher instance " + loader);
    }
    String[] getPatchedClasses = (String[]) aClass
    .getDeclaredMethod("getPatchedClasses", new Class[0])
    .invoke(loader, new Object[0]);
    if (Log.isLoggable("InstantRun", 2)) {
    Log.v("InstantRun", "Got the list of classes ");
    for (String getPatchedClass : getPatchedClasses) {
    Log.v("InstantRun", "class " + getPatchedClass);
    }
    }
    if (!loader.load()) {
    updateMode = 3;
    }
    } catch (Exception e) {
    Log.e("InstantRun", "Couldn't apply code changes", e);
    e.printStackTrace();
    updateMode = 3;
    }
    } catch (Throwable e) {
    Log.e("InstantRun", "Couldn't apply code changes", e);
    updateMode = 3;
    }
    return updateMode;
    }

    說明:該方法將patch的dex文件寫入到臨時目錄,然后使用DexClassLoader去加載dex。然后反射調用AppPatchesLoaderImpl類的load方法。

    需要強調的是:AppPatchesLoaderImpl繼承自抽象類AbstractPatchesLoaderImpl,并實現了抽象方法:getPatchedClasses。而AbstractPatchesLoaderImpl抽象類代碼如下:

    public abstract class AbstractPatchesLoaderImpl implements PatchesLoader {
    public abstract String[] getPatchedClasses();
    public boolean load() {
    try {
    for (String className : getPatchedClasses()) {
    ClassLoader cl = getClass().getClassLoader();
    Class aClass = cl.loadClass(className + "$override");
    Object o = aClass.newInstance();
    Class originalClass = cl.loadClass(className);
    Field changeField = originalClass.getDeclaredField("$change");
    changeField.setAccessible(true);
    Object previous = changeField.get(null);
    if (previous != null) {
    Field isObsolete = previous.getClass().getDeclaredField(
    "$obsolete");
    if (isObsolete != null) {
    isObsolete.set(null, Boolean.valueOf(true));
    }
    }
    changeField.set(null, o);
    if ((Log.logging != null)
    && (Log.logging.isLoggable(Level.FINE))) {
    Log.logging.log(Level.FINE, String.format("patched %s",
    new Object[] { className }));
    }
    }
    } catch (Exception e) {
    if (Log.logging != null) {
    Log.logging.log(Level.SEVERE, String.format(
    "Exception while patching %s",
    new Object[] { "foo.bar" }), e);
    }
    return false;
    }
    return true;
    }
    }

    Instant Run熱部署原理

    由上面的代碼分析,我們對Instant Run的流程可以分析如下:

    1,在第一次構建apk時,在每一個類中注入了一個$change的成員變量,它實現了IncrementalChange接口,并在每一個方法中,插入了一段類似的邏輯。

    IncrementalChange localIncrementalChange = $change;
    if (localIncrementalChange != null) {
    localIncrementalChange.access$dispatch(
    "onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
    ... });
    return;
    }

    當$change不為空的時候,執行IncrementalChange方法。

    2,當我們修改代碼中方法的實現之后,點擊InstantRun,它會生成對應的patch文件來記錄你修改的內容。patch文件中的替換類是在所修改類名的后面追加$override,并實現IncrementalChange接口。

    3,生成AppPatchesLoaderImpl類,繼承自AbstractPatchesLoaderImpl,并實現getPatchedClasses方法,來記錄哪些類被修改了。

    4,調用load方法之后,根據getPatchedClasses返回的修改過的類的列表,去加載對應的$override類,然后把原有類的$change設置為對應的實現了IncrementalChange接口的$override類。

    Instant Run運行機制總結

    Instant Run運行機制主要涉及到熱部署、溫部署和冷部署,主要是在第一次運行,app運行時期,有代碼修改時。

    第一次編譯

    1.把Instant-Run.jar和instant-Run-bootstrap.jar打包到主dex中

    2.替換AndroidManifest.xml中的application配置

    3.使用asm工具,在每個類中添加$change,在每個方法前加邏輯

    4.把源代碼編譯成dex,然后存放到壓縮包instant-run.zip中

    app運行時

    1.獲取更改后資源resource.ap_的路徑

    2.設置ClassLoader。setupClassLoader:

    使用IncrementalClassLoader加載apk的代碼,將原有的BootClassLoader → PathClassLoader改為BootClassLoader → IncrementalClassLoader → PathClassLoader繼承關系。

    3.createRealApplication:

    創建apk真實的application

    4.monkeyPatchApplication

    反射替換ActivityThread中的各種Application成員變量

    5.monkeyPatchExistingResource

    反射替換所有存在的AssetManager對象

    6.調用realApplication的onCreate方法

    7.啟動Server,Socket接收patch列表

    有代碼修改時

    1.生成對應的$override類

    2.生成AppPatchesLoaderImpl類,記錄修改的類列表

    3.打包成patch,通過socket傳遞給app

    4.app的server接收到patch之后,分別按照handleColdSwapPatch、handleHotSwapPatch、handleResourcePatch等待對patch進行處理

    5.restart使patch生效

    在Android插件化、Android熱修復、apk加殼/脫殼中借鑒了Instant Run運行機制,所以理解Instant Run運行機制對于向更深層次的研究是很有幫助的,對于我們自己書寫框架也是有借鑒意義的。

     

    來自:https://yq.aliyun.com/articles/72566?utm_campaign=wenzhang&utm_medium=article&utm_source=QQ-qun&2017323&utm_content=m_14685

     

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