背景
Android studio 2.0有一个新特性-Instanct Run,可以在不重启App的情况下运行修改后的代码。具体使用方法可以参考官方文档,接下来我们具体分析下Instant Run的实现原理。
原理
涉及到的工具
- dex2jar
- jd-gui
涉及到的Jar包
- instant-run.jar
- 反编译后的apk
打开反编译后的apk,我们可以很清晰的看到多了2个包,com.android.build.gradle.internal.incremental和com.android.tools,之后我们就会发现其实这2个包就是instance-run.jar,在build期间被打包到apk里面。
这部分我们先不管,我们先看下编写的代码里面变化了什么。
打出的Patch包
FloatingActionButtonBasicFragment$override
我们可以发现每一个函数里面都多了一个$change,当 $change不为null时,执行access$dispatch,否则执行旧逻辑。我们可以猜测是com.android.tools.build:gradle:2.0.0-alpha1处理的。
接下来我们再看看之前我们留下的2个新增包,看看都做了什么。
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和已存在的资源,然后创建Server,该Server主要处理读取客户端的Dex文件,如果用更新,则进行加载和处理。
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时,读取数据之后,进行处理。
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 "); 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);
}
我们可以看到,先进行一些简单的校验,判断读取的数据是否正确。然后依次读取文件数据。
- 如果读到7,则表示已经读到文件的末尾,退出读取操作
- 如果读到2,则表示获取当前Activity活跃状态,并且进行记录
- 如果读到3,读取UTF-8字符串路径,读取该路径下文件长度,并且进行记录
- 如果读到4,读取UTF-8字符串路径,获取该路径下文件MD5值,如果没有,则记录0,否则记录MD5值和长度。
- 如果读到5,先校验输入的值是否正确(根据token来判断),如果正确,则在UI线程重启Activity
- 如果读到1,先校验输入的值是否正确(根据token来判断),如果正确,获取代码变化的List,处理代码的改变(handlePatches,这个之后具体分析),然后重启
- 如果读到6,读取UTF-8字符串,showToast
handlePatches
private int handlePatches(@NonNull List 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。接下来我们具体来看。
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) {
//创建下一个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类,初始化,执行load方法
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的关键,还记得多出来的$change吗?在运行程序的时候,就可以根据该变量,执行被替换的函数。
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);
}
现在我们终于理清了Instant Run的原理,大家有不明白的可以留言。这是初稿,之后会优化。