背景
Android studio 2.0有一個(gè)新特性-Instanct Run,可以在不重啟App的情況下運(yùn)行修改后的代碼。具體使用方法可以參考官方文檔,接下來(lái)我們具體分析下Instant Run的實(shí)現(xiàn)原理。
原理
涉及到的工具
- dex2jar
- jd-gui
涉及到的Jar包
- instant-run.jar
- 反編譯后的apk
打開反編譯后的apk,我們可以很清晰的看到多了2個(gè)包,com.android.build.gradle.internal.incremental和com.android.tools,之后我們就會(huì)發(fā)現(xiàn)其實(shí)這2個(gè)包就是instance-run.jar,在build期間被打包到apk里面。
這部分我們先不管,我們先看下編寫的代碼里面變化了什么。
打出的Patch包
FloatingActionButtonBasicFragment$override
我們可以發(fā)現(xiàn)每一個(gè)函數(shù)里面都多了一個(gè)$change,當(dāng) $change不為null時(shí),執(zhí)行access$dispatch,否則執(zhí)行舊邏輯。我們可以猜測(cè)是com.android.tools.build:gradle:2.0.0-alpha1處理的。
接下來(lái)我們?cè)倏纯粗拔覀兞粝碌?個(gè)新增包,看看都做了什么。
BootstrapApplication:
onCreate
public void onCreate()
{
MonkeyPatcher.monkeyPatchApplication(this, this, this.realApplication, this.externalResourcePath);
MonkeyPatcher.monkeyPatchExistingResources(this, this.externalResourcePath, null);
super.onCreate();
if (AppInfo.applicationId != null) {
Server.create(AppInfo.applicationId, this);
}
if (this.realApplication != null)
this.realApplication.onCreate();
}
先Monkey Application和已存在的資源,然后創(chuàng)建Server,該Server主要處理讀取客戶端的Dex文件,如果用更新,則進(jìn)行加載和處理。
Server
SocketServerThread
private class SocketServerThread extends Thread
{
private SocketServerThread()
{
}
public void run()
{
try
{
while (true)
{
LocalServerSocket serverSocket = Server.this.mServerSocket;
if (serverSocket == null) {
break;
}
LocalSocket socket = serverSocket.accept();
if (Log.isLoggable("fd", 4)) {
Log.i("fd", "Received connection from IDE: spawning connection thread");
}
Server.SocketServerReplyThread socketServerReplyThread = new Server.SocketServerReplyThread(Server.this, socket);
socketServerReplyThread.run();
if (Server.mWrongTokenCount > 50) {
if (Log.isLoggable("fd", 4)) {
Log.i("fd", "Stopping server: too many wrong token connections");
}
Server.this.mServerSocket.close();
break;
}
}
} catch (IOException e) {
if (Log.isLoggable("fd", 4))
Log.i("fd", "Fatal error accepting connection on local socket", e);
}
}
}
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) {
}
}
} catch (IOException e) {
if (Log.isLoggable("fd", 4))
Log.i("fd", "Fatal error receiving messages", e);
}
}
開啟Socket時(shí),讀取數(shù)據(jù)之后,進(jìn)行處理。
private void handle(DataInputStream input, DataOutputStream output) throws IOException
{
long magic = input.readLong();
if (magic != 890269988L) {
Log.w("fd", "Unrecognized header format " + Long.toHexString(magic));
return;
}
int version = input.readInt();
output.writeInt(4);
if (version != 4) {
Log.w("fd", "Mismatched protocol versions; app is using version 4 and tool is using version " + version);
return;
}
int message;
while (true) {
message = input.readInt();
switch (message) {
case 7:
if (Log.isLoggable("fd", 4)) {
Log.i("fd", "Received EOF from the IDE");
}
return;
case 2:
boolean active = Restarter.getForegroundActivity(Server.this.mApplication) != null;
output.writeBoolean(active);
if (!Log.isLoggable("fd", 4)) continue;
Log.i("fd", "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("fd", 4)) continue;
Log.i("fd", "Received path-exists(" + path + ") from the " + "IDE; returned size=" + size); break;
case 4:
long begin = System.currentTimeMillis();
String path = input.readUTF();
byte[] checksum = FileManager.getCheckSum(path);
if (checksum != null) {
output.writeInt(checksum.length);
output.write(checksum);
if (!Log.isLoggable("fd", 4)) continue;
long end = System.currentTimeMillis();
String hash = new BigInteger(1, checksum).toString(16);
Log.i("fd", "Received checksum(" + path + ") from the " + "IDE: took " + (end - begin) + "ms to compute " + hash);
continue;
}
output.writeInt(0);
if (!Log.isLoggable("fd", 4)) continue;
Log.i("fd", "Received checksum(" + path + ") from the " + "IDE: returning <null>"); break;
case 5:
if (!authenticate(input)) {
return;
}
Activity activity = Restarter.getForegroundActivity(Server.this.mApplication);
if (activity == null) continue;
if (Log.isLoggable("fd", 4)) {
Log.i("fd", "Restarting activity per user request");
}
Restarter.restartActivityOnUiThread(activity); break;
case 1:
if (!authenticate(input)) {
return;
}
List changes = ApplicationPatch.read(input);
if (changes == null)
{
continue;
}
boolean hasResources = Server.this.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); continue;
}if (!Log.isLoggable("fd", 4)) continue;
Log.i("fd", "Couldn't show toast (no activity) : " + text);
}
}
if (Log.isLoggable("fd", 6))
Log.e("fd", "Unexpected message type: " + message);
}
我們可以看到,先進(jìn)行一些簡(jiǎn)單的校驗(yàn),判斷讀取的數(shù)據(jù)是否正確。然后依次讀取文件數(shù)據(jù)。
- 如果讀到7,則表示已經(jīng)讀到文件的末尾,退出讀取操作
- 如果讀到2,則表示獲取當(dāng)前Activity活躍狀態(tài),并且進(jìn)行記錄
- 如果讀到3,讀取UTF-8字符串路徑,讀取該路徑下文件長(zhǎng)度,并且進(jìn)行記錄
- 如果讀到4,讀取UTF-8字符串路徑,獲取該路徑下文件MD5值,如果沒(méi)有,則記錄0,否則記錄MD5值和長(zhǎng)度。
- 如果讀到5,先校驗(yàn)輸入的值是否正確(根據(jù)token來(lái)判斷),如果正確,則在UI線程重啟Activity
- 如果讀到1,先校驗(yàn)輸入的值是否正確(根據(jù)token來(lái)判斷),如果正確,獲取代碼變化的List,處理代碼的改變(handlePatches,這個(gè)之后具體分析),然后重啟
- 如果讀到6,讀取UTF-8字符串,showToast
handlePatches
private int handlePatches(@NonNull List<ApplicationPatch> changes, boolean hasResources, int updateMode)
{
if (hasResources) {
FileManager.startUpdate();
}
for (ApplicationPatch change : changes) {
String path = change.getPath();
if (path.endsWith(".dex"))
handleColdSwapPatch(change);
else if (path.endsWith(".dex.3"))
updateMode = handleHotSwapPatch(updateMode, change);
else {
updateMode = handleResourcePatch(updateMode, change, path);
}
}
if (hasResources) {
FileManager.finishUpdate(true);
}
return updateMode;
}
如果文件路徑后綴是".dex",則handleColdSwapPatch,如果后綴是".dex.3",則handleHotSwapPatch,否則handleResourcePatch。接下來(lái)我們具體來(lái)看。
handleColdSwapPatch
private void handleColdSwapPatch(@NonNull ApplicationPatch patch) {
if (Log.isLoggable("fd", 4)) {
Log.i("fd", "Received restart code patch");
}
FileManager.writeDexFile(patch.getBytes(), true);
}
寫入Dex文件
writeDexFile
public static File writeDexFile(@NonNull byte[] bytes, boolean writeIndex) {
//創(chuàng)建下一個(gè)Dex文件,
File file = getNextDexFile();
if (file != null) {
writeRawBytes(file, bytes);
if (writeIndex) {
File indexFile = getIndexFile(file);
try {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(indexFile), getUtf8Charset()));
DexFile dexFile = new DexFile(file);
Enumeration entries = dexFile.entries();
while (entries.hasMoreElements()) {
String nextPath = (String)entries.nextElement();
if (nextPath.indexOf(36) != -1)
{
continue;
}
writer.write(nextPath);
writer.write(10);
}
writer.close();
if (Log.isLoggable("fd", 4))
Log.i("fd", "Wrote restart patch index " + indexFile);
}
catch (IOException ioe) {
Log.e("fd", "Failed to write dex index file " + indexFile);
}
}
}
return file;
}
handleHotSwapPatch
private int handleHotSwapPatch(int updateMode, @NonNull ApplicationPatch patch)
{
if (Log.isLoggable("fd", 4))
Log.i("fd", "Received incremental code patch");
try
{
//寫入Dex文件
String dexFile = FileManager.writeTempDexFile(patch.getBytes());
if (dexFile == null) {
Log.e("fd", "No file to write the code to");
return updateMode;
}if (Log.isLoggable("fd", 4)) {
Log.i("fd", "Reading live code from " + dexFile);
}
String nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath();
DexClassLoader dexClassLoader = new DexClassLoader(dexFile, this.mApplication.getCacheDir().getPath(), nativeLibraryPath, getClass().getClassLoader());
//加載AppPatchesLoaderImpl類,初始化,執(zhí)行l(wèi)oad方法
Class aClass = Class.forName("com.android.build.gradle.internal.incremental.AppPatchesLoaderImpl", true, dexClassLoader);
try {
if (Log.isLoggable("fd", 4)) {
Log.i("fd", "Got the patcher class " + aClass);
}
PatchesLoader loader = (PatchesLoader)aClass.newInstance();
if (Log.isLoggable("fd", 4)) {
Log.i("fd", "Got the patcher instance " + loader);
}
String[] getPatchedClasses = (String[])(String[])aClass.getDeclaredMethod("getPatchedClasses", new Class[0]).invoke(loader, new Object[0]);
if (Log.isLoggable("fd", 4)) {
Log.i("fd", "Got the list of classes ");
for (String getPatchedClass : getPatchedClasses) {
Log.i("fd", "class " + getPatchedClass);
}
}
if (!loader.load())
updateMode = 3;
}
catch (Exception e) {
Log.e("fd", "Couldn't apply code changes", e);
e.printStackTrace();
updateMode = 3;
}
} catch (Throwable e) {
Log.e("fd", "Couldn't apply code changes", e);
updateMode = 3;
}
return updateMode;
}
AbstractPatchesLoaderImpl
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);
Log.i("fd", String.format("patched %s", new Object[] { className }));
}
} catch (Exception e) {
Log.e("fd", String.format("Exception while patching %s", new Object[] { "foo.bar" }), e);
return false;
}
return true;
}
加載class名稱+override類,給$change賦值,這就是Instance Run的關(guān)鍵,還記得多出來(lái)的$change嗎?在運(yùn)行程序的時(shí)候,就可以根據(jù)該變量,執(zhí)行被替換的函數(shù)。
handleResourcePatch
private int handleResourcePatch(int updateMode, @NonNull ApplicationPatch patch, @NonNull String path)
{
if (Log.isLoggable("fd", 4)) {
Log.i("fd", "Received resource changes (" + path + ")");
}
FileManager.writeAaptResources(path, patch.getBytes());
updateMode = Math.max(updateMode, 2);
return updateMode;
}
寫入aapt Resource
public static void writeAaptResources(@NonNull String relativePath, @NonNull byte[] bytes)
{
File resourceFile = getResourceFile(getWriteFolder(false));
File file = resourceFile;
File folder = file.getParentFile();
if (!folder.isDirectory()) {
boolean created = folder.mkdirs();
if (!created) {
if (Log.isLoggable("fd", 4)) {
Log.i("fd", "Cannot create local resource file directory " + folder);
}
return;
}
}
if (relativePath.equals("resources.ap_"))
{
writeRawBytes(file, bytes);
}
else
writeRawBytes(file, bytes);
}
現(xiàn)在我們終于理清了Instant Run的原理,大家有不明白的可以留言。這是初稿,之后會(huì)優(yōu)化。