深入理解 Android Instant Run 執行機制

code_xzh發表於2017-03-24

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(Gradle修改class的原理,請戳連結)。

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

說明:該方法主要是判斷資源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);
}
}

說明,該方法是初始化一個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;
}
}

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

該方法就是用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);
}
}

說明:該方法的作用是替換所有當前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;
}
}
}
}
}

說明: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執行機制對於向更深層次的研究是很有幫助的,對於我們自己書寫框架也是有借鑑意義的。

相關文章