/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.fd.runtime;
import android.app.Activity;
import android.app.Application;
import android.net.LocalServerSocket;
import android.net.LocalSocket;
import android.os.Handler;
import android.util.Log;
import com.android.annotations.NonNull;
import dalvik.system.DexClassLoader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import static com.android.tools.fd.common.ProtocolConstants.MESSAGE_EOF;
import static com.android.tools.fd.common.ProtocolConstants.MESSAGE_PATCHES;
import static com.android.tools.fd.common.ProtocolConstants.MESSAGE_PATH_CHECKSUM;
import static com.android.tools.fd.common.ProtocolConstants.MESSAGE_PATH_EXISTS;
import static com.android.tools.fd.common.ProtocolConstants.MESSAGE_PING;
import static com.android.tools.fd.common.ProtocolConstants.MESSAGE_RESTART_ACTIVITY;
import static com.android.tools.fd.common.ProtocolConstants.MESSAGE_SHOW_TOAST;
import static com.android.tools.fd.common.ProtocolConstants.PROTOCOL_IDENTIFIER;
import static com.android.tools.fd.common.ProtocolConstants.PROTOCOL_VERSION;
import static com.android.tools.fd.common.ProtocolConstants.UPDATE_MODE_COLD_SWAP;
import static com.android.tools.fd.common.ProtocolConstants.UPDATE_MODE_HOT_SWAP;
import static com.android.tools.fd.common.ProtocolConstants.UPDATE_MODE_NONE;
import static com.android.tools.fd.common.ProtocolConstants.UPDATE_MODE_WARM_SWAP;
import static com.android.tools.fd.runtime.BootstrapApplication.LOG_TAG;
import static com.android.tools.fd.runtime.FileManager.CLASSES_DEX_SUFFIX;
import static com.android.tools.fd.runtime.Paths.RELOAD_DEX_FILE_NAME;
import static com.android.tools.fd.runtime.Paths.RESOURCE_FILE_NAME;
/**
* Server running in the app listening for messages from the IDE and updating the code and
* resources
* when provided
*
* 1. LocalServerSocket 的使用
* 2. SocketServerThread 的设计
* 3. 资源校验( res/resources.ap_ )
* 4. 处理补丁:
* - 4.1 dex 结尾的格式,就执行 handleColdSwapPatch(...) 冷部署
* - 4.2 dex 结尾的格式 并且 名字为 "classes.dex.3" 则记录为 热部署
* - 4.3 名字为 "classes.dex.3" 直接执行热部署 handleHotSwapPatch(…)
* - 4.4 "res/resources.ap_" 那么直接处理资源补丁 handleResourcePatch(…)
* - 4.5:
* - - 4.5.1 温部署 加载补丁 ( 处理资源补丁 ):调用 FileManager.writeAaptResources(...) 处理资源补丁
* - - 4.5.2 热部署 加载补丁:
* - - - <1> 将补丁文件 保存为 /data/data/.../files/instant-run/dex-temp/reload0x?04x.dex
* - - - <2> 然后 通过 此 dex 去创建一个 DexClassLoader
* - - - <3> 通过创建的 DexClassLoader 去寻找内部的 AppPatchesLoaderImpl 类
* - - - <4> 进而获取 getPatchedClasses 方法,得到 String[] classes
* - - - <5> 然后打 String[] classes 的 Log
* - - - <6> AppPatchesLoaderImpl 向上转为 PatchesLoader 类型
* - - - <7> 调用 ( AppPatchesLoaderImpl )PatchesLoader.load() 方法打上 $override 和 $change 标记位
* - - 4.5.3 冷部署 加载补丁:
* - - - <1> 判断补丁是否是 slice- 开头
* - - - <2> 将补丁保存在 /data/data/.../files/instant-run/dex/ 目录下
* 5. 重启流程处理:
* - 5.1 热部署:如果更新模式 是 None 或者 热部署。如果要显示 toast。获取前台 Activity,然后用 前台 Activity 显示 toast,然后返回
* - 5.2 冷部署:
* - - <1> 获取所有没有 paused 的 Activity
* - - <2> 获取外部资源文件路径 /data/data/.../files/instant-run/left(right)/resources.ap_
* - - <3.1> 如果不存在资源文件:MonkeyPatcher.monkeyPatchApplication + MonkeyPatcher.monkeyPatchExistingResources
* - - <3.2> 如果存在存在资源文件:设置更新模式 - 冷部署
* - 5.3 温部署:
* - - <1> 先拿到前台显示的 Activity
* - - <2> 如果是 温部署 :
* - - - <2.1> 然后反射获取 onHandleCodeChange 方法,进而传入 0L 为参数,进行反射调用
* - - - <2.2> 如果,刚才的 handledRestart 标记为 true,那么继续显示 toast,然后重启 Activity 后返回
* - - - <2.3> 最后将更新模式设置为 冷部署
* - - <3> 判断更新模式如果是冷部署则返回( 证明没成功调用 onHandleCodeChange )
*/
public class Server {
/**
* If true, app restarts itself after receiving coldswap patches. If false,
* it will just wait for the client to kill it remotely and restart via activity manager.
* If we restart locally, there could be problems around: a) getting all the right intent
* data to the restarted activity, and b) sometimes, the activity state is saved by the
* system, and it could lead to conflicts with the new version of the app.
* So this is currently turned off. See
* https://code.google.com/p/android/issues/detail?id=200895#c9
*
* true: app 重启自己后,接受 冷部署 补丁
* false:只会等待远程客户端将其杀死 并重启 Activity manager
*/
private static final boolean RESTART_LOCALLY = false;
/**
* Temporary debugging: have the server emit a message to the log every 30 seconds to
* indicate whether it's still alive
*/
private static final boolean POST_ALIVE_STATUS = false;
private LocalServerSocket mServerSocket;
private final Application mApplication;
private static int sWrongTokenCount;
/**
* 对外提供的 静态工厂方法
* 用于构造一个 Server
*
* @param packageName packageName
* @param application application
*/
public static void create(@NonNull String packageName, @NonNull Application application) {
//noinspection ResultOfObjectAllocationIgnored
new Server(packageName, application);
}
/**
* 私有构造方法
* 主要是实例化一个 LocalServerSocket
* 然后调用 startServer() 方法
*
* @param packageName packageName
* @param application application
*/
private Server(@NonNull String packageName, @NonNull Application application) {
mApplication = application;
try {
mServerSocket = new LocalServerSocket(packageName);
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Starting server socket listening for package " + packageName
+ " on " + mServerSocket.getLocalSocketAddress());
}
} catch (IOException e) {
Log.e(LOG_TAG, "IO Error creating local socket at " + packageName, e);
return;
}
startServer();
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Started server for package " + packageName);
}
}
/**
* 创建 并 启动 Socket server thread
*/
private void startServer() {
try {
Thread socketServerThread = new Thread(new SocketServerThread());
socketServerThread.start();
} catch (Throwable e) {
// Make sure an exception doesn't cause the rest of the user's
// onCreate() method to be invoked
if (Log.isLoggable(LOG_TAG, Log.ERROR)) {
Log.e(LOG_TAG, "Fatal error starting Instant Run server", e);
}
}
}
private class SocketServerThread extends Thread {
@Override
public void run() {
/**
* Step 1
*
* 如果 POST_ALIVE_STATUS 标记为 true
* 则每 30 秒打一次 Log
*/
if (POST_ALIVE_STATUS) {
final Handler handler = new Handler();
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
handler.post(new Runnable() {
@Override
public void run() {
Log.v(LOG_TAG, "Instant Run server still here...");
}
});
}
};
timer.schedule(task, 1, 30000L);
}
/**
* Step 2
*
* 进入一个循环体
* 拿到当前的 Local Server Socket,如果为 null,则退出循环体
*
* 然后开始监听数据( accept ),监听到数据( LocalSocket )后
* 然后 new 一个 SocketServerReplyThread 去处理 LocalSocket
*
* 如果之前记录的错误次数( sWrongTokenCount )大于 50 次
* 那么,就关闭 Local Server Socket,并且退出循环体
*/
while (true) {
try {
LocalServerSocket serverSocket = mServerSocket;
if (serverSocket == null) {
break; // stopped?
}
LocalSocket socket = serverSocket.accept();
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Received connection from IDE: spawning connection thread");
}
SocketServerReplyThread socketServerReplyThread = new SocketServerReplyThread(
socket);
socketServerReplyThread.run();
if (sWrongTokenCount > 50) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Stopping server: too many wrong token connections");
}
mServerSocket.close();
break;
}
} catch (Throwable e) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Fatal error accepting connection on local socket", e);
}
}
}
}
}
private class SocketServerReplyThread extends Thread {
private final LocalSocket mSocket;
SocketServerReplyThread(LocalSocket socket) {
mSocket = socket;
}
/**
* 获取传入 LocalSocket 内的数据流
* DataInputStream 和 DataOutputStream
* 交给 handle(...) 方法去处理业务
*/
@Override
public void run() {
try {
DataInputStream input = new DataInputStream(mSocket.getInputStream());
DataOutputStream output = new DataOutputStream(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(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Fatal error receiving messages", e);
}
}
}
/**
* 开头先获取了 magic number,判断是否与协定好的 Magic number( PROTOCOL_IDENTIFIER )一致
* 不一致的话,就视为脏数据,return
*
* 读取 version,然后通过 DataOutputStream 告诉 IDE version
* 然后,判断 version 是否与协定好的版本( PROTOCOL_VERSION )一致
* 不一致的话,视为版本不一致,return
*
* 然后进入循环体,开始处理 message
*
* MESSAGE_EOF: 已经读到文件的末尾,退出读取操作
* MESSAGE_PING: 获取当前活动活跃状态,并且进行记录
* MESSAGE_PATH_EXISTS: 读取 文件路径,读取该路径下文件长度,并且进行记录
* MESSAGE_PATH_CHECKSUM: 读取 resources.ap_ 文件路径,获取 resources.ap_ 文件的 MD5 值
* 如果 resources.ap_ 文件路径有文件,记录文件的 MD5 值和长度
* 否则 记录 0
* MESSAGE_RESTART_ACTIVITY:验证 token 后,如果 token 正确,则在 UI 线程重启 Activity
* MESSAGE_PATCHES:
* 1. 验证 token,不匹配则返回
* 2. 不断读取 补丁,没有则跳过此次的消息处理
* 3. 有补丁,判断其内部是否有资源,记录为 hasResources。拿到 updateMode 后, handlePatches(...) 处理补丁,同时拿到修改后的
* 更新模式( updateMode )
* 4. 读取 readBoolean(),是否显示 toast,然后记下为 showToast
* 5. 重新启动 restart。通过 updateMode, showToast, hasResources 来决定 Activity 显示的 toast 信息
* 以及是什么 部署策略
* MESSAGE_SHOW_TOAST: 读取 提示 信息,然后获取当前前台 Activity。如果有,就 show toast
*
* @param input input
* @param output output
* @throws IOException
*/
private void handle(DataInputStream input, DataOutputStream output) throws IOException {
long magic = input.readLong();
if (magic != PROTOCOL_IDENTIFIER) {
Log.w(LOG_TAG, "Unrecognized header format "
+ Long.toHexString(magic));
return;
}
int version = input.readInt();
// Send current protocol version to the IDE so it can decide what to do
output.writeInt(PROTOCOL_VERSION);
if (version != PROTOCOL_VERSION) {
Log.w(LOG_TAG, "Mismatched protocol versions; app is "
+ "using version " + PROTOCOL_VERSION + " and tool is using version "
+ version);
return;
}
while (true) {
int message = input.readInt();
switch (message) {
case MESSAGE_EOF: {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Received EOF from the IDE");
}
return;
}
case MESSAGE_PING: {
// Send an "ack" back to the IDE.
// The value of the boolean is true only when the app is in the
// foreground.
boolean active = Restarter.getForegroundActivity(mApplication) != null;
output.writeBoolean(active);
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Received Ping message from the IDE; " +
"returned active = " + active);
}
continue;
}
case MESSAGE_PATH_EXISTS: {
String path = input.readUTF();
long size = FileManager.getFileSize(path);
output.writeLong(size);
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Received path-exists(" + path + ") from the " +
"IDE; returned size=" + size);
}
continue;
}
case MESSAGE_PATH_CHECKSUM: {
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(LOG_TAG, Log.VERBOSE)) {
long end = System.currentTimeMillis();
String hash = new BigInteger(1, checksum).toString(16);
Log.v(LOG_TAG, "Received checksum(" + path + ") from the " +
"IDE: took " + (end - begin) + "ms to compute " + hash);
}
} else {
output.writeInt(0);
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Received checksum(" + path + ") from the " +
"IDE: returning <null>");
}
}
continue;
}
case MESSAGE_RESTART_ACTIVITY: {
if (!authenticate(input)) {
return;
}
Activity activity = Restarter.getForegroundActivity(mApplication);
if (activity != null) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Restarting activity per user request");
}
Restarter.restartActivityOnUiThread(activity);
}
continue;
}
case MESSAGE_PATCHES: {
if (!authenticate(input)) {
return;
}
List<ApplicationPatch> changes = ApplicationPatch.read(input);
if (changes == null) {
continue;
}
boolean hasResources = hasResources(changes);
int updateMode = input.readInt();
updateMode = handlePatches(changes, hasResources, updateMode);
boolean showToast = input.readBoolean();
// Send an "ack" back to the IDE; this is used for timing purposes only
output.writeBoolean(true);
restart(updateMode, hasResources, showToast);
continue;
}
case MESSAGE_SHOW_TOAST: {
String text = input.readUTF();
Activity foreground = Restarter.getForegroundActivity(mApplication);
if (foreground != null) {
Restarter.showToast(foreground, text);
} else if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Couldn't show toast (no activity) : " + text);
}
continue;
}
default: {
if (Log.isLoggable(LOG_TAG, Log.ERROR)) {
Log.e(LOG_TAG, "Unexpected message type: " + message);
}
// If we hit unexpected message types we can't really continue
// the conversation: we can misinterpret data for the unexpected
// command as separate messages with different meanings than intended
return;
}
}
}
}
/**
* 校验 DataInputStream 内的 token 是否与 AppInfo 的 token 一致
*
* @param input DataInputStream
* @return true or false
* @throws IOException
*/
private boolean authenticate(@NonNull DataInputStream input) throws IOException {
long token = input.readLong();
if (token != AppInfo.token) {
Log.w(LOG_TAG, "Mismatched identity token from client; received " + token
+ " and expected " + AppInfo.token);
sWrongTokenCount++;
return false;
}
return true;
}
}
/**
* 校验 资源 name( resources.ap_ ) 和 路径是否以 res/ 开头
* res/resources.ap_
*
* @param path 资源路径
* @return 校验结果
*/
private static boolean isResourcePath(String path) {
return path.equals(RESOURCE_FILE_NAME) || path.startsWith("res/");
}
/**
* 判断 补丁 内是否有 路径
* 然后进行 是否是 资源路径 ( res/resources.ap_ )的校验
*
* @param changes 补丁集
* @return 是否有资源
*/
private static boolean hasResources(@NonNull List<ApplicationPatch> changes) {
// Any non-code patch is a resource patch (normally resources.ap_ but could
// also be individual resource files such as res/layout/activity_main.xml)
for (ApplicationPatch change : changes) {
String path = change.getPath();
if (isResourcePath(path)) {
return true;
}
}
return false;
}
/**
* 处理补丁
*
* @param changes 补丁集
* @param hasResources 是否有资源
* @param updateMode 更新模式
* @return 更新模式
*/
private int handlePatches(@NonNull List<ApplicationPatch> changes, boolean hasResources,
int updateMode) {
if (hasResources) {
FileManager.startUpdate();
}
/**
* 检查 补丁 路径
* 1.
* 1.1 .dex 结尾的格式,就执行 handleColdSwapPatch(...) 冷部署,并且记录为 冷部署 更新模式
* 1.2 寻找 补丁集合内 是否有 .dex 结尾的格式 并且 名字为 "classes.dex.3" 则记录为 热部署 的更新模式( updateMode )
* 2. 如果 名字为 "classes.dex.3" 直接执行热部署 handleHotSwapPatch(...),并记录更新模式
* 3. 如果 是 "res/resources.ap_" 那么直接处理资源补丁 handleResourcePatch(...),并记录更新模式
* 4. 返回更新模式
*/
for (ApplicationPatch change : changes) {
String path = change.getPath();
if (path.endsWith(CLASSES_DEX_SUFFIX)) {
handleColdSwapPatch(change);
// Gradle sometimes sends a restart dex even when there is a hotswap patch,
// so don't take the presence of a restart dex as a conclusion that we must
// do a coldswap. Check.
boolean canHotSwap = false;
for (ApplicationPatch c : changes) {
if (c.getPath().equals(RELOAD_DEX_FILE_NAME)) {
canHotSwap = true;
break;
}
}
if (!canHotSwap) {
updateMode = UPDATE_MODE_COLD_SWAP;
}
} else if (path.equals(RELOAD_DEX_FILE_NAME)) {
updateMode = handleHotSwapPatch(updateMode, change);
} else if (isResourcePath(path)) {
updateMode = handleResourcePatch(updateMode, change, path);
}
}
if (hasResources) {
FileManager.finishUpdate(true);
}
return updateMode;
}
/**
* 温部署 加载补丁 ( 处理资源补丁 )
*
* 内部实质上调用 FileManager.writeAaptResources(...) 处理资源补丁
* Math.max(updateMode, UPDATE_MODE_WARM_SWAP) 也只有 冷部署 比 温部署大
* 返回的模式只可能是 冷部署 or 温部署
*
* @param updateMode 更新模式
* @param patch 补丁类
* @param path 补丁路径
* @return 更新模式
*/
private static int handleResourcePatch(int updateMode, @NonNull ApplicationPatch patch,
@NonNull String path) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Received resource changes (" + path + ")");
}
FileManager.writeAaptResources(path, patch.getBytes());
//noinspection ResourceType
updateMode = Math.max(updateMode, UPDATE_MODE_WARM_SWAP);
return updateMode;
}
/**
* 热部署 加载补丁
*
* 1. 将补丁文件 保存为 /data/data/.../files/instant-run/dex-temp/reload0x????.dex
* 2. 然后 通过 此 dex 去创建一个 DexClassLoader
* 3. 通过创建的 DexClassLoader 去寻找内部的 AppPatchesLoaderImpl类
* 4. 进而获取 getPatchedClasses 方法,得到 String[] classes
* 5. 然后打 String[] classes 的 Log
* 6. AppPatchesLoaderImpl 向上转为 PatchesLoader 类型
* 7. 调用 ( AppPatchesLoaderImpl )PatchesLoader.load() 方法打上 $override 和 $change 标记位
*
* @param updateMode 更新模式
* @param patch 补丁
* @return 更新模式
*/
private int handleHotSwapPatch(int updateMode, @NonNull ApplicationPatch patch) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Received incremental code patch");
}
try {
String dexFile = FileManager.writeTempDexFile(patch.getBytes());
if (dexFile == null) {
Log.e(LOG_TAG, "No file to write the code to");
return updateMode;
} else if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Reading live code from " + dexFile);
}
String nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath();
DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
mApplication.getCacheDir().getPath(), nativeLibraryPath,
getClass().getClassLoader());
// we should transform this process with an interface/impl
Class<?> aClass = Class.forName(
"com.android.tools.fd.runtime.AppPatchesLoaderImpl", true, dexClassLoader);
try {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Got the patcher class " + aClass);
}
PatchesLoader loader = (PatchesLoader) aClass.newInstance();
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Got the patcher instance " + loader);
}
String[] getPatchedClasses = (String[]) aClass
.getDeclaredMethod("getPatchedClasses").invoke(loader);
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Got the list of classes ");
for (String getPatchedClass : getPatchedClasses) {
Log.v(LOG_TAG, "class " + getPatchedClass);
}
}
if (!loader.load()) {
updateMode = UPDATE_MODE_COLD_SWAP;
}
} catch (Exception e) {
Log.e(LOG_TAG, "Couldn't apply code changes", e);
e.printStackTrace();
updateMode = UPDATE_MODE_COLD_SWAP;
}
} catch (Throwable e) {
Log.e(LOG_TAG, "Couldn't apply code changes", e);
updateMode = UPDATE_MODE_COLD_SWAP;
}
return updateMode;
}
/**
* 冷部署 加载补丁
* 1. 判断补丁是否是 slice- 开头
* 2. 将补丁保存在 /data/data/.../files/instant-run/dex/ 目录下
*
* @param patch 补丁
*/
private static void handleColdSwapPatch(@NonNull ApplicationPatch patch) {
if (patch.path.startsWith(Paths.DEX_SLICE_PREFIX)) {
File file = FileManager.writeDexShard(patch.getBytes(), patch.path);
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Received dex shard " + file);
}
}
}
/**
* 重启
*
* @param updateMode 更新模式
* @param incrementalResources 插件是否有资源( 有增量资源 )
* @param toast 是否显示 toast
*/
private void restart(int updateMode, boolean incrementalResources, boolean toast) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Finished loading changes; update mode =" + updateMode);
}
/**
* Step 1( 热部署 )
*
*
* 1. 如果更新模式 是 None 或者 热部署。
* 如果要显示 toast。获取前台 Activity,然后用 前台 Activity 显示 toast,然后返回
*/
if (updateMode == UPDATE_MODE_NONE || updateMode == UPDATE_MODE_HOT_SWAP) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Applying incremental code without restart");
}
if (toast) {
Activity foreground = Restarter.getForegroundActivity(mApplication);
if (foreground != null) {
Restarter.showToast(foreground, "Applied code changes without activity " +
"restart");
} else if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Couldn't show toast: no activity found");
}
}
return;
}
/**
* Step 2( 冷部署 )
*
* 1. 获取所有没有 paused 的 Activity
* 2. 获取外部资源文件路径 /data/data/.../files/instant-run/left(right)/resources.ap_
* 3.
* 3.1 如果不存在资源文件:MonkeyPatcher.monkeyPatchApplication + MonkeyPatcher.monkeyPatchExistingResources
* 3.2 如果存在存在资源文件:设置更新模式 - 冷部署
*/
List<Activity> activities = Restarter.getActivities(mApplication, false);
if (incrementalResources && updateMode == UPDATE_MODE_WARM_SWAP) {
// Try to just replace the resources on the fly!
File file = FileManager.getExternalResourceFile();
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "About to update resource file=" + file +
", activities=" + activities);
}
if (file != null) {
String resources = file.getPath();
MonkeyPatcher.monkeyPatchApplication(mApplication, null, null, resources);
MonkeyPatcher.monkeyPatchExistingResources(mApplication, resources, activities);
} else {
Log.e(LOG_TAG, "No resource file found to apply");
updateMode = UPDATE_MODE_COLD_SWAP;
}
}
/**
* Step 3( 温部署 )
*
* 1. 先拿到前台显示的 Activity
* 2. 如果是 温部署
* 2.1 然后反射获取 onHandleCodeChange 方法,进而传入 0L 为参数,进行反射调用
* 2.2 如果成功调用则记下标记为 handledRestart,同时显示 toast
* 2.3 如果,刚才的 handledRestart 标记为 true,那么继续显示 toast,然后重启 Activity 后返回。
* 2.4 最后将更新模式设置为 冷部署
* 3. 判断更新模式如果是冷部署则返回( 证明没成功调用 onHandleCodeChange )。
*/
Activity activity = Restarter.getForegroundActivity(mApplication);
if (updateMode == UPDATE_MODE_WARM_SWAP) {
if (activity != null) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Restarting activity only!");
}
boolean handledRestart = false;
try {
// Allow methods to handle their own restart by implementing
// public boolean onHandleCodeChange(long flags) { .... }
// and returning true if the change was handled manually
Method method = activity.getClass().getMethod("onHandleCodeChange", Long.TYPE);
Object result = method.invoke(activity, 0L);
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Activity " + activity
+ " provided manual restart method; return " + result);
}
if (Boolean.TRUE.equals(result)) {
handledRestart = true;
if (toast) {
Restarter.showToast(activity, "Applied changes");
}
}
} catch (Throwable ignore) {
}
if (!handledRestart) {
if (toast) {
Restarter.showToast(activity, "Applied changes, restarted activity");
}
Restarter.restartActivityOnUiThread(activity);
}
return;
}
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "No activity found, falling through to do a full app restart");
}
updateMode = UPDATE_MODE_COLD_SWAP;
}
if (updateMode != UPDATE_MODE_COLD_SWAP) {
if (Log.isLoggable(LOG_TAG, Log.ERROR)) {
Log.e(LOG_TAG, "Unexpected update mode: " + updateMode);
}
return;
}
if (RESTART_LOCALLY) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Performing full app restart");
}
Restarter.restartApp(mApplication, activities, toast);
} else {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Waiting for app to be killed and restarted by the IDE...");
}
}
}
}