/*
* 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.content.Context;
import android.util.Log;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static com.android.tools.fd.runtime.AppInfo.applicationId;
import static com.android.tools.fd.runtime.BootstrapApplication.LOG_TAG;
/**
* Class which handles locating existing code and resource files on the device,
* as well as writing new versions of these.
*
* 最核心方法:
*
* 1. checkInbox: 复制资源文件 resources.ap_( 主要用于 创建资源 )
* - 1.1 判断 /data/data/( applicationId )/files/instant-run/inbox/resources.ap_ 是否存在
* - 1.2 存在的话,复制到 /data/data/.../files/instant-run/left(or right)/resources.ap_ 下
* 2. getDexList: 获取 /data/data/( applicationId )/files/instant-run/dex 的 .dex 路径集合
* - ( 主要用于 HOOK BootstrapApplication 的 ClassLoader 的 类加载机制 )
* - 2.1 获取 /data/data/( applicationId )/files/instant-run/dex-temp 文件夹下,最近修改的.dex 文
* - 件的更新时间,记录为 newestHotswapPatch
* - 2.2 获取 File: /data/data/( applicationId )/files/instant-run/dex ,但不一定创建
* - 2.3 校验 /data/data/( applicationId )/files/instant-run/dex 文件夹:
* - 2.3.1 如果不存在,那么会创建该文件夹后,将 instant-run.zip 内的所有 .dex ,加上前缀
* - "slice-" 复制到 /data/data/( applicationId )/files/instant-run/dex 文件夹 中
* - 最后,获取该文件夹内的所有文件,保存在 File[] dexFiles
* - 2.3.2 如果直接存在,直接获取 /data/data/( applicationId )/files/instant-run/dex 文件
* - 夹中的所有文件,保存在 File[] dexFiles
* - 2.4 如果 2.3 内提取 instant-run.zip :
* - 2.4.1 失败了。再次校验 /data/data/( applicationId )/files/instant-run/dex 文件夹。遍历所有
* - 文件,如果有一个文件的修改时间小于 APK 的修改时间,证明存在旧的 dex。将 instant-run.zip
* - 内的所有 .dex ,加上前缀"slice-" 复制到 /data/data/( applicationId )/files/instant-run/dex
* - 文件夹 中。然后,清空不是提取复制过来的 dex( 旧 dex )。
* - 2.4.2 成功了。判断 1. 中的 dex-temp 文件夹是否存在 dex。存在的话,清空 dex-temp 文件夹
* - 2.5 最后判断 hotSwap 的时间是不是比 coldSwap 的时间新。实质上就是 dex-temp 文件夹内的 files 和
* - dex 文件夹内的 files,谁最新!如果 hotSwap 的时间比 coldSwap 的时间新,调用 Restarter.showToastWhenPossible
* - 提示 the app is older
* - 2.6 返回 /data/data/( applicationId )/files/instant-run/dex 的 .dex 路径集合
* 3. extractSlices: 提取 instant-run.zip 的资源( 主要用于获取所有 dex 集合,然后实例化一个补丁 ClassLoader
* - 进而 HOOK BootstrapApplication 的 ClassLoader 的 类加载机制 )
* - 3.1 Class.getResourceAsStream("/instant-run.zip") 去加载 instant-run.zip 的资源
* - 3.2 提取出 instant-run.zip 内的资源( 内部都是 .dex 文件 ):
* - 3.2.1 过滤掉 META-INF
* - 3.2.2 过滤掉有 "/" 的 文件或文件夹
* - 3.2.3 找出所有 .dex 文件,将其文件名加上前缀 "slice-" 保存在 Set<String> sliceNames
* - 3.2.4 再将这些 .dex 文件,加载前缀 "slice-",复制到 /data/data/( applicationId )/files/instant-run/dex
* - 文件夹中
* - 3.2.5 校验 /data/data/( applicationId )/files/instant-run/dex 文件夹中,是非存在不是 3.2.4
* _ 复制过来的文件。如果不是 2.4 复制过来的文件,证明是旧 "slice-" 文件,则删除
* 4. getTempDexFile: 获取 dex-temp 文件夹下,下版本要创建的 File ( 主要用于 热部署 )
* - 4.1 获取 /data/data/( applicationId )/files/instant-run/dex-temp 文件夹
* - 4.2 校验 dex-temp 文件夹:
* - 4.2.1 不存在,则创建
* - 4.2.2 存在,则判断是否要清空。清空的话,则删除该文件夹下的所有 .dex 文件
* - 4.3 然后遍历 dex-temp 文件夹下的文件:
* - 4.3.1 截断 "reload" 和 ".dex" 之间的 十六进制版本号
* - 4.3.2 找出版本号最大的 .dex 文件
* - 4.4 根据 4.3.2 的找出的最大版本号的基础上,最大版本号+1,然后创建一个 "reload最大版本号.dex"
* - 的 File 返回
* 5. writeRawBytes: 二进制 生成 文件 ( 所有部署 )。主要将 二进制数据 输出为 resources.ap_ or .dex
* 6. extractZip: 提取出 instant-run.zip 流 内的 .dex 文件 ( 主要用于 温部署 )
* - 注: 这提取出的 .dex ,不带 "slice-" 前缀。与 extractSlices 方法不同
* - 6.1 过滤掉 META-INF
* - 6.2 如果父路径文件夹不存在,则创建
* 7. writeDexShard: 生成 dex( 主要用于 冷部署 )
* - 7.1 校验目录:/data/data/( applicationId )/files/instant-run/dex。没有,则创建
* - 7.2 通过调用 writeRawBytes 方法,在该目录下保存 dex 文件
* 8. writeAaptResources: 生成资源文件 resources 或者 resources.ap_ ( 主要用于 温部署 )
* - 路径一般为 /data/data/( applicationId )/files/instant-run/left( right )
* - /resources.ap_( resources )
* - 8.1 拿到以上路径后,创建该路径的父文件夹
* - 8.2 生成资源文件:
* - 8.2.1 如果生成 resources.ap_:
* - 8.2.1.1 如果 USE_EXTRACTED_RESOURCES = true,那么该流为 instant-run.zip 的数据,直接复
* - 制出内部的 dex 到 /data/data/( applicationId )/files/instant-run/left( right )
* - 目录下
* - 8.2.1.2 如果 USE_EXTRACTED_RESOURCES = false,生成
* - /data/data/( applicationId )/files/instant-run/left( right )/resources.ap_
* - 8.2.2 如果生成 resources,那么直接写出
* - /data/data/( applicationId )/files/instant-run/left( right )/resources
* 9. writeTempDexFile: 在 dex-temp 文件夹下 生成 dex ( 主要用于 热部署 )
* 10. purgeTempDexFiles: 清空 dex-temp 下的 .dex 文件 ( 用于清空 热部署 产生的 dex-temp 文件夹中的 dex )
* 11. readRawBytes: 读取 inbox/resources.ap_ ( 主要用于创建资源时,读取 inbox/resources.ap_ )
* - 11.1 路径为: /data/data/( applicationId )/files/instant-run/inbox/resources.ap_
* - 11.2 为了复制到 /data/data/.../files/instant-run/left(or right)/resources.ap_
*
* >>>>>>>
*
* 文件以及目录归类:
*
* 热部署:
* /data/data/( applicationId )/files/instant-run/dex-temp
*
* 温部署:
* /instant-run.zip
* /data/data/( applicationId )/files/instant-run/left( right )
* /data/data/( applicationId )/files/instant-run/inbox/resources.ap_
* /data/data/( applicationId )/files/instant-run/left(or right)/resources.ap_
*
* 冷部署:
* /data/data/( applicationId )/files/instant-run/dex
*/
public class FileManager {
/**
* According to Dianne, using an extracted directory tree of resources rather than
* in an archive was implemented before 1.0 and never used or tested... so we should
* tread carefully here.
*
* 标识是否 使用提取资源
*/
private static final boolean USE_EXTRACTED_RESOURCES = false;
/**
* 资源文件名 resources.ap_
*/
/** Name of file to write resource data into, if not extracting resources */
private static final String RESOURCE_FILE_NAME = Paths.RESOURCE_FILE_NAME;
/**
* 资源文件夹 resources
*/
/** Name of folder to write extracted resource data into, if extracting resources */
private static final String RESOURCE_FOLDER_NAME = "resources";
/**
* 用于指定文件话 left 还是 right 的 文件名 active
*/
/** Name of the file which points to either the left or the right data directory */
private static final String FILE_NAME_ACTIVE = "active";
/**
* left 文件夹
*/
/** Name of the left directory */
private static final String FOLDER_NAME_LEFT = "left";
/**
* right 文件夹
*/
/** Name of the right directory */
private static final String FOLDER_NAME_RIGHT = "right";
/**
* reload.dex 的前缀
*/
/** Prefix for reload.dex files */
private static final String RELOAD_DEX_PREFIX = "reload";
/**
* classes.dex 的扩展名
*/
/** Suffix for classes.dex files */
public static final String CLASSES_DEX_SUFFIX = ".dex";
/**
* 标识是否 清空 temp dex 文件
*/
/** Whether we've purged temp dex files in this session */
private static boolean sHavePurgedTempDexFolder;
/**
* The folder where resources and code are located. Within this folder we have two
* alternatives: "left" and "right". One is in the foreground (in use), one is in the
* background (to write to). These are named {@link #FOLDER_NAME_LEFT} and
* {@link #FOLDER_NAME_RIGHT} and the current one is pointed to by
* {@link #FILE_NAME_ACTIVE}.
*
* 获取数据目录:/data/data/( applicationId )/files/instant-run
*/
private static File getDataFolder() {
// TODO: Call Context#getFilesDir(), but since we don't have a context yet figure
// out what to do
// Keep in sync with ResourceDeltaManager in the IDE (which needs this path
// in order to run an adb wipe command when reinstalling a freshly built app
// to avoid using stale data)
return new File(Paths.getDataDirectory(applicationId));
}
/**
* 获取资源文件
*
* 1. 如果 使用提取资源,那么路径为 父路径/resource
* 2. 如果 不使用提取资源,那么路径为 父路径/resources.ap_
*
* 父路径:
* left: /data/data/( applicationId )/files/instant-run/left
* 还是
* right: /data/data/( applicationId )/files/instant-run/right
*
* @param base 父路径
* @return 资源完整路径
*/
@NonNull
private static File getResourceFile(File base) {
//noinspection ConstantConditions
return new File(base, USE_EXTRACTED_RESOURCES ? RESOURCE_FOLDER_NAME : RESOURCE_FILE_NAME);
}
/**
* 获取 dex 文件夹
*
* 去寻找 父路径/dex 文件夹
* 一般都是 /data/data/( applicationId )/files/instant-run/dex
* 根据 createIfNecessary 的值,考虑不存在的话,是否创建
*
* @param base 父路径
* @param createIfNecessary 如果不存在,是否创建
* @return File = 父路径/dex or null
*/
/**
* Returns the folder used for .dex files used during the next app start
*/
@Nullable
private static File getDexFileFolder(File base, boolean createIfNecessary) {
File file = new File(base, Paths.DEX_DIRECTORY_NAME);
if (createIfNecessary) {
if (!file.isDirectory()) {
boolean created = file.mkdirs();
if (!created) {
Log.e(LOG_TAG, "Failed to create directory " + file);
return null;
}
}
}
return file;
}
/**
* 获取临时 dex 文件夹
*
* 直接 new 一个 File = /data/data/( applicationId )/files/instant-run/dex-temp
* 一般都是 /data/data/( applicationId )/files/instant-run/dex-temp
*
* @param base /data/data/( applicationId )/files/instant-run
* @return File = /data/data/( applicationId )/files/instant-run/dex-temp
*/
/**
* Returns the folder used for temporary .dex files (e.g. classes loaded on the fly
* and only needing to exist during the current app process
*/
@NonNull
private static File getTempDexFileFolder(File base) {
return new File(base, "dex-temp");
}
/**
* 获取本地 lib 文件夹
*
* 直接 new 一个 File = /data/data/( applicationId )/lib
*
* @return File = /data/data/( applicationId )/lib
*/
public static File getNativeLibraryFolder() {
return new File(Paths.getMainApkDataDirectory(applicationId), "lib");
}
/**
* 获取 外部资源读取的 文件夹
*
* 根据 {@link FileManager#leftIsActive} 的结果,决定读取
*
* left: /data/data/( applicationId )/files/instant-run/left
* 还是
* right: /data/data/( applicationId )/files/instant-run/right
*
* 主要用于 {@link FileManager#getExternalResourceFile}
*
* @return left or right
*/
/**
* Returns the "foreground" folder: the location to read code and resources from.
*/
@NonNull
public static File getReadFolder() {
String name = leftIsActive() ? FOLDER_NAME_LEFT : FOLDER_NAME_RIGHT;
return new File(getDataFolder(), name);
}
/**
* 反转文件夹
*
* 如果 leftIsActive() 表示 true,表示 left
* 如果 leftIsActive() 表示 false,表示 right
*
* 但是 setLeftActive(!leftIsActive()) 之后,
* 会清空之前的 active 的内容,leftIsActive() 表示 true,那么在 active 文件写入 right,
* leftIsActive() 表示 false,那么在 active 文件写入 left
* 达到反转文件夹的效果
*/
/**
* Swaps the read/write folders such that the next time somebody asks for the
* read or write folders, they'll get the opposite.
*/
public static void swapFolders() {
setLeftActive(!leftIsActive());
}
/**
* 获取 外部资源写入 的文件夹
*
* 根据 {@link FileManager#leftIsActive} 的结果,决定写入
*
* left: /data/data/( applicationId )/files/instant-run/left
* 还是
* right: /data/data/( applicationId )/files/instant-run/right
*
* 然后再根据 wipe ,来决定是否 一定! 删除或者保留之前 left( or right ) 的文件夹
*
* @param wipe 是否清空之前的文件
* @return 外部资源写入 的文件夹
*/
/**
* Returns the "background" folder: the location to write code and resources to.
*/
@NonNull
public static File getWriteFolder(boolean wipe) {
String name = leftIsActive() ? FOLDER_NAME_RIGHT : FOLDER_NAME_LEFT;
File folder = new File(getDataFolder(), name);
if (wipe && folder.exists()) {
delete(folder);
boolean mkdirs = folder.mkdirs();
if (!mkdirs) {
Log.e(LOG_TAG, "Failed to create folder " + folder);
}
}
return folder;
}
/**
* 删除 文件夹 or 文件
*
* 文件:直接删除
* 文件夹:删除文件以及文件夹
*
* @param file 要删除的文件
*/
private static void delete(@NonNull File file) {
if (file.isDirectory()) {
// Delete the contents
File[] files = file.listFiles();
if (files != null) {
for (File child : files) {
delete(child);
}
}
}
//noinspection ResultOfMethodCallIgnored
boolean deleted = file.delete();
if (!deleted) {
Log.e(LOG_TAG, "Failed to delete file " + file);
}
}
/**
* 校验 active 文件或内容
*
* 1. 先拿到 data 目录: /data/data/( applicationId )/files/instant-run
* 2. 定义 File : /data/data/( applicationId )/files/instant-run/active
* 3. 如果 active 文件不存在,则返回 true,断定为 left
* 4. 尝试读取 active 的内容
* - 4.1 如果读到 "left",返回 true,断定为 left
* - 4.2 如果读到 "right", 返回 false,断定为 right
* - 4.3 如果什么都没读到,或者文件不存在等等问题,返回 true,默认断定为 left
*
* @return true,断定为 left 或者 false,断定为 right
*/
private static boolean leftIsActive() {
File folder = getDataFolder();
File pointer = new File(folder, FILE_NAME_ACTIVE);
if (!pointer.exists()) {
return true;
}
try {
BufferedReader reader = new BufferedReader(new FileReader(pointer));
try {
String line = reader.readLine();
return FOLDER_NAME_LEFT.equals(line);
} finally {
reader.close();
}
} catch (IOException ignore) {
return true;
}
}
/**
* 创建 active 文件
*
* 创建 active 文件,并根据传入的 boolean,写入 "left" 还是 "right"
*
* 1. 先拿到 data 目录: /data/data/( applicationId )/files/instant-run
* 2. 定义 File : /data/data/( applicationId )/files/instant-run/active
* 3. 判断 active 是否存在
* - 3.1 存在,则删除
* - 3.2 不存在,并且其父路径也不存在,则创建父路径的文件夹
* 4. 根据 active 值,开始创建 active 文件,并对其写入内容
* - 4.1 如果 active = true,写入 "left"
* - 4.2 如果 active = false,写入 "right"
*
* @param active active
*/
private static void setLeftActive(boolean active) {
File folder = getDataFolder();
File pointer = new File(folder, FILE_NAME_ACTIVE);
if (pointer.exists()) {
boolean deleted = pointer.delete();
if (!deleted) {
Log.e(LOG_TAG, "Failed to delete file " + pointer);
}
} else if (!folder.exists()) {
boolean create = folder.mkdirs();
if (!create) {
Log.e(LOG_TAG, "Failed to create directory " + folder);
}
return;
}
try {
Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(pointer),
"UTF-8"));
try {
writer.write(active ? FOLDER_NAME_LEFT : FOLDER_NAME_RIGHT);
} finally {
writer.close();
}
} catch (IOException ignore) {
}
}
/**
* 复制资源文件 resources.ap_( 主要用于 创建资源 )
*
* 1. 判断 /data/data/( applicationId )/files/instant-run/inbox/resources.ap_ 是否存在
* 2. 存在的话,复制到 /data/data/.../files/instant-run/left(or right)/resources.ap_ 下
*
* 主要用于 {@link BootstrapApplication#createResources(long)}
*/
/** Looks in the inbox for new changes sent while the app wasn't running and apply them */
public static void checkInbox() {
File inbox = new File(Paths.getInboxDirectory(applicationId));
if (inbox.isDirectory()) {
File resources = new File(inbox, RESOURCE_FILE_NAME);
if (resources.isFile()) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Processing resource file from inbox (" + resources + ")");
}
byte[] bytes = readRawBytes(resources);
if (bytes != null) {
FileManager.startUpdate();
FileManager.writeAaptResources(RESOURCE_FILE_NAME, bytes);
FileManager.finishUpdate(true);
boolean deleted = resources.delete();
if (!deleted) {
if (Log.isLoggable(LOG_TAG, Log.ERROR)) {
Log.e(LOG_TAG, "Couldn't remove inbox resource file: " + resources);
}
}
}
}
}
}
/**
* 获取 外部资源文件
*
* 根据 {@link FileManager#leftIsActive} 的结果,决定读取
*
* left: /data/data/( applicationId )/files/instant-run/left
* 还是
* right: /data/data/( applicationId )/files/instant-run/right
*
* 如果不存在则返回 null,存在则 返回 left or right
*
* @return null,left,right
*/
/** Returns the current/active resource file, if it exists */
@Nullable
public static File getExternalResourceFile() {
File file = getResourceFile(getReadFolder());
if (!file.exists()) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Cannot find external resources, not patching them in");
}
return null;
}
return file;
}
/**
* 获取 /data/data/( applicationId )/files/instant-run/dex 的 .dex 路径集合
* ( 主要用于 HOOK BootstrapApplication 的 ClassLoader 的 类加载机制 )
*
* 1. 获取 /data/data/( applicationId )/files/instant-run/dex-temp 文件夹下,最近修改的.dex 文
* - 件的更新时间,记录为 newestHotswapPatch
* 2. 获取 File: /data/data/( applicationId )/files/instant-run/dex ,但不一定创建
* 3. 校验 /data/data/( applicationId )/files/instant-run/dex 文件夹:
* - 3.1 如果不存在,那么会创建该文件夹后,将 instant-run.zip 内的所有 .dex ,加上前缀
* "slice-" 复制到 /data/data/( applicationId )/files/instant-run/dex 文件夹 中
* 最后,获取该文件夹内的所有文件,保存在 File[] dexFiles
* - 3.2 如果直接存在,直接获取 /data/data/( applicationId )/files/instant-run/dex 文件
* - 夹中的所有文件,保存在 File[] dexFiles
* 4. 如果 3. 内提取 instant-run.zip :
* - 4.1 失败了。再次校验 /data/data/( applicationId )/files/instant-run/dex 文件夹。遍历所有文
* - 件,如果有一个文件的修改时间小于 APK 的修改时间,证明存在旧的 dex。将 instant-run.zip 内
* - 的所有 .dex ,加上前缀"slice-" 复制到 /data/data/( applicationId )/files/instant-run/dex
* - 文件夹 中。然后,清空不是提取复制过来的 dex( 旧 dex )。
* - 4.2 成功了。判断 1. 中的 dex-temp 文件夹是否存在 dex。存在的话,清空 dex-temp 文件夹
* 5. 最后判断 hotSwap 的时间是不是比 coldSwap 的时间新。实质上就是 dex-temp 文件夹内的 files 和 dex 文
* - 件夹内的 files,谁最新!如果 hotSwap 的时间比 coldSwap 的时间新,调用 Restarter.showToastWhenPossible
* - 提示 the app is older
* 6. 返回 /data/data/( applicationId )/files/instant-run/dex 的 .dex 路径集合
*
* @param context context
* @param apkModified the apk modified time
* @return /data/data/( applicationId )/files/instant-run/dex 的 .dex 路径集合
*/
/**
* Returns the list of available .dex files to be loaded, possibly empty
*
* @param apkModified main apk installation time to purge old dex files from previous
* installation.
*/
@NonNull
public static List<String> getDexList(Context context, long apkModified) {
File dataFolder = getDataFolder();
long newestHotswapPatch = FileManager.getMostRecentTempDexTime(dataFolder);
// We don't need "double buffering" for dex files - we never rewrite files, so we
// can accumulate in the same dir
File dexFolder = getDexFileFolder(dataFolder, false);
// Extract slices.
//
// Imagine this scenario -- you run your app (so the device dex folder is filled).
// Then you do a clean build etc -- so Gradle doesn't know there is existing state
// on the device. If we *only* extract slices when there are no slices there already,
// then we'd end up here just running the old slices already on the device.
// On the other hand, we can't just always extract slices, since then each time
// you run we'll overwrite coldswap and freezeswap slices.
//
// So what this code does is pass the APK timestamp to the extractor, and in the
// extractor, if the timestamp is positive, we check before writing each slice that
// it doesn't already exist and is newer than the APK.
boolean extractedSlices = false;
File[] dexFiles;
if (dexFolder == null || !dexFolder.isDirectory()) {
// It's the first run of a freshly installed app, and we need to extract the
// slices from within the APK into the dex folder
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "No local dex slice folder: First run since installation.");
}
dexFolder = getDexFileFolder(dataFolder, true);
if (dexFolder == null) {
// Failed to create dex folder.
Log.wtf(LOG_TAG, "Couldn't create dex code folder");
return Collections.emptyList(); // unreachable
}
dexFiles = extractSlices(dexFolder, null, -1); // -1: unconditionally extract all
extractedSlices = dexFiles.length > 0;
} else {
dexFiles = dexFolder.listFiles();
}
if (dexFiles == null || dexFiles.length == 0) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Cannot find dex classes, not patching them in");
}
return Collections.emptyList();
}
// See if any of the slices are older than the APK. This will only be the case
// if it's not the first run, and the APK has been reinstalled while there are some
// potentially stale dex files.
//
// Note that we're *also* computing the timestamp of the *newest* coldswap slice.
// We'll use that below to post a toast if the app seems to be missing hotswap patches.
long newestColdswapPatch = apkModified;
if (!extractedSlices && dexFiles.length > 0) {
long oldestColdSwapPatch = apkModified;
for (File dex : dexFiles) {
long dexModified = dex.lastModified();
oldestColdSwapPatch = Math.min(dexModified, oldestColdSwapPatch);
newestColdswapPatch = Math.max(dexModified, newestColdswapPatch);
}
if (oldestColdSwapPatch < apkModified) {
// At least one slice is older than the APK: re-extract those that
// need it
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG,
"One or more slices were older than APK: extracting newer slices");
}
dexFiles = extractSlices(dexFolder, dexFiles, apkModified);
}
} else if (newestHotswapPatch > 0L) {
// If the code is newer than the hotswap patches, delete them such that we don't
// have to keep iterating through them each successive startup
purgeTempDexFiles(dataFolder);
}
if (newestHotswapPatch > newestColdswapPatch) {
String message = "Your app does not have the latest code changes because it "
+ "was restarted manually. Please run from IDE instead.";
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, message);
}
// We now want to show a toast to the user showing that the app is older.
// However, it's too early to show it here:
Restarter.showToastWhenPossible(context, message);
}
List<String> list = new ArrayList<String>(dexFiles.length);
for (File dex : dexFiles) {
if (dex.getName().endsWith(CLASSES_DEX_SUFFIX)) {
list.add(dex.getPath());
}
}
// Dex files should be sorted in reverse order such that the class loader finds
// most recent updates first
Collections.sort(list, Collections.reverseOrder());
return list;
}
/**
* 提取 instant-run.zip 的资源( 主要用于获取所有 dex 集合,然后实例化一个补丁 ClassLoader,进而
* HOOK BootstrapApplication 的 ClassLoader 的 类加载机制 )
*
* 1. Class.getResourceAsStream("/instant-run.zip") 去加载 instant-run.zip 的资源
* 2. 提取出 instant-run.zip 内的资源( 内部都是 .dex 文件 ):
* - 2.1 过滤掉 META-INF
* - 2.2 过滤掉有 "/" 的 文件或文件夹
* - 2.3 找出所有 .dex 文件,将其文件名加上前缀 "slice-" 保存在 Set<String> sliceNames
* - 2.4 再将这些 .dex 文件,加载前缀 "slice-",复制到 /data/data/( applicationId )/files/instant-run/dex
* - 文件夹中
* - 2.5 校验 /data/data/( applicationId )/files/instant-run/dex 文件夹中,是非存在不是 2.4 复制过来的文件
* - 如果不是 2.4 复制过来的文件,证明是旧 "slice-" 文件,则删除
*
* @param dexFolder /data/data/( applicationId )/files/instant-run/dex
* @param dexFolderFiles /data/data/( applicationId )/files/instant-run/dex 内原本存在的文件
* @param apkModified APK 的最后修改时间
* @return instant-run.zip 的 .dex 加上前缀后的文件集合
*/
/**
* Extracts the slices found in the APK root directory (instant-run.zip) into the dex folder,
* and skipping any files that already exist and are newer than apkModified (unless apkModified
* <= 0). It <b>also</b> deletes any <b>unrecognized</b> slices. This is necessary
* since there are scenarios (such as b.android.com/204341) where we end up with slice files
* in the dex folder that should <b>not</b> be loaded.
*/
private static File[] extractSlices(@NonNull File dexFolder, @Nullable File[] dexFolderFiles,
long apkModified) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Extracting slices into " + dexFolder);
}
InputStream stream = BootstrapApplication.class.getResourceAsStream("/instant-run.zip");
if (stream == null) {
if (Log.isLoggable(LOG_TAG, Log.ERROR)) {
Log.e(LOG_TAG, "Could not find slices in APK; aborting.");
}
return new File[0];
}
List<File> slices = new ArrayList<File>(30);
Set<String> sliceNames = new HashSet<String>(30);
try {
ZipInputStream zipInputStream = new ZipInputStream(stream);
try {
byte[] buffer = new byte[2000];
for (ZipEntry entry = zipInputStream.getNextEntry();
entry != null;
entry = zipInputStream.getNextEntry()) {
String name = entry.getName();
// Don't extract META-INF data
if (name.startsWith("META-INF")) {
continue;
}
if (!entry.isDirectory()
&& name.indexOf('/') == -1 // only files in root directory
&& name.endsWith(CLASSES_DEX_SUFFIX)) {
// Using / as separators in both .zip files and on Android, no need to convert
// to File.separator
// Map slice name to the scheme already used by the code to push slices
// via the embedded server as well as the code to push via adb:
// slice-<slicedir>
String sliceName = Paths.DEX_SLICE_PREFIX + name;
sliceNames.add(sliceName);
File dest = new File(dexFolder, sliceName);
slices.add(dest);
if (apkModified > 0) {
long sliceModified = dest.lastModified();
if (sliceModified > apkModified) {
// Ignore this slice: disk copy more recent than APK copy
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Ignoring slice " + name
+ ": newer on disk than in APK");
}
continue;
}
}
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Extracting slice " + name + " into " + dest);
}
File parent = dest.getParentFile();
if (parent != null && !parent.exists()) {
boolean created = parent.mkdirs();
if (!created) {
Log.wtf(LOG_TAG, "Failed to create directory " + dest);
return new File[0];
}
}
OutputStream src = new BufferedOutputStream(new FileOutputStream(dest));
try {
int bytesRead;
while ((bytesRead = zipInputStream.read(buffer)) != -1) {
src.write(buffer, 0, bytesRead);
}
} finally {
src.close();
}
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "File written at " + System.currentTimeMillis());
Log.v(LOG_TAG, "File last modified reported : " + dest.lastModified());
}
}
}
// Remove old slice names
if (dexFolderFiles != null) {
for (File file : dexFolderFiles) {
if (!sliceNames.contains(file.getName())) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Removing old slice " + file);
}
boolean deleted = file.delete();
if (!deleted) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Could not delete " + file);
}
}
}
}
}
return slices.toArray(new File[slices.size()]);
} catch (IOException ioe) {
Log.wtf(LOG_TAG, "Failed to extract slices into directory " + dexFolder, ioe);
return new File[0];
} finally {
try {
zipInputStream.close();
} catch (IOException ignore) {
}
}
} finally {
try {
stream.close();
} catch (IOException ignore) {
}
}
}
/**
* 获取 dex-temp 文件夹下,下版本要创建的 File ( 主要用于 热部署 )
*
* 1. 获取 /data/data/( applicationId )/files/instant-run/dex-temp 文件夹
* 2. 校验 dex-temp 文件夹:
* - 2.1 不存在,则创建
* - 2.2 存在,则判断是否要清空。清空的话,则删除该文件夹下的所有 .dex 文件
* 3. 然后遍历 dex-temp 文件夹下的文件:
* - 3.1 截断 "reload" 和 ".dex" 之间的 十六进制版本号
* - 3.2 找出版本号最大的 .dex 文件
* 4. 根据 3.2 的找出的最大版本号的基础上,最大版本号+1,然后创建一个 "reload最大版本号.dex"
* - 的 File 返回
*
* @return dex-temp 文件夹下,下版本要创建的 File
*/
/** Produces the next available dex file name */
@Nullable
public static File getTempDexFile() {
// Find the file name of the next dex file to write
File dataFolder = getDataFolder();
File dexFolder = getTempDexFileFolder(dataFolder);
if (!dexFolder.exists()) {
boolean created = dexFolder.mkdirs();
if (!created) {
Log.e(LOG_TAG, "Failed to create directory " + dexFolder);
return null;
}
// There was nothing to purge, but leave the folder be from now on.
sHavePurgedTempDexFolder = true;
} else {
// The *first* time we write a reload dex file in the new process, we'll
// delete previously stashes reload dex files. (We keep them around
// such that we can (repeatedly) warn an app on startup if its hotswap patches
// are more recent than the app itself, such that developers aren't confused
// when the app is not reflecting the most recent changes
if (!sHavePurgedTempDexFolder) {
purgeTempDexFiles(dataFolder);
}
}
File[] files = dexFolder.listFiles();
int max = -1;
// Pick highest available number + 1 - we want these to be sortable
if (files != null) {
for (File file : files) {
String name = file.getName();
if (name.startsWith(RELOAD_DEX_PREFIX) && name.endsWith(CLASSES_DEX_SUFFIX)) {
String middle = name.substring(RELOAD_DEX_PREFIX.length(),
name.length() - CLASSES_DEX_SUFFIX.length());
try {
int version = Integer.decode(middle);
if (version > max) {
max = version;
}
} catch (NumberFormatException ignore) {
}
}
}
}
String fileName = String.format("%s0x%04x%s", RELOAD_DEX_PREFIX, max + 1,
CLASSES_DEX_SUFFIX);
File file = new File(dexFolder, fileName);
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Writing new dex file: " + file);
}
return file;
}
/**
* 二进制 生成 文件 ( 所有部署 )
*
* 主要将 二进制数据 输出为 resources.ap_ or .dex
*
* @param destination 路径
* @param bytes 二进制数据
* @return 是否成功
*/
public static boolean writeRawBytes(@NonNull File destination, @NonNull byte[] bytes) {
try {
BufferedOutputStream output = new BufferedOutputStream(
new FileOutputStream(destination));
try {
output.write(bytes);
output.flush();
return true;
} finally {
output.close();
}
} catch (IOException ioe) {
Log.wtf(LOG_TAG, "Failed to write file, clean project and rebuild " + destination, ioe);
throw new RuntimeException(
String.format(
"InstantRun could not write file %1$s, clean project and rebuild ",
destination));
}
}
public static boolean extractZip(@NonNull File destination, @NonNull byte[] zipBytes) {
InputStream inputStream = new ByteArrayInputStream(zipBytes);
return extractZip(destination, inputStream);
}
/**
* 提取出 instant-run.zip 流 内的 .dex 文件 ( 主要用于 温部署 )
*
* 1. 过滤掉 META-INF
* 2. 如果父路径文件夹不存在,则创建
*
* 注: 这提取出的 .dex ,不带 "slice-" 前缀
*
* 和 {@link FileManager#extractSlices(File, File[], long)} 不一样
*
* @param destDir 目标文件夹
* @param inputStream 数据流
* @return 是否提取成功
*/
public static boolean extractZip(@NonNull File destDir, @NonNull InputStream inputStream) {
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
try {
byte[] buffer = new byte[2000];
for (ZipEntry entry = zipInputStream.getNextEntry();
entry != null;
entry = zipInputStream.getNextEntry()) {
String name = entry.getName();
// Don't extract META-INF data
if (name.startsWith("META-INF")) {
continue;
}
if (!entry.isDirectory()) {
// Using / as separators in both .zip files and on Android, no need to convert
// to File.separator
File dest = new File(destDir, name);
File parent = dest.getParentFile();
if (parent != null && !parent.exists()) {
boolean created = parent.mkdirs();
if (!created) {
Log.e(LOG_TAG, "Failed to create directory " + dest);
return false;
}
}
OutputStream src = new BufferedOutputStream(new FileOutputStream(dest));
try {
int bytesRead;
while ((bytesRead = zipInputStream.read(buffer)) != -1) {
src.write(buffer, 0, bytesRead);
}
} finally {
src.close();
}
}
}
return true;
} catch (IOException ioe) {
Log.e(LOG_TAG, "Failed to extract zip contents into directory " + destDir, ioe);
return false;
} finally {
try {
zipInputStream.close();
} catch (IOException ignore) {
}
}
}
/**
* 创建 外部资源写入 的文件夹
*
* 1. 如果存在,则先删除该文件夹
* 2. 然后再开始创建 left or right
*
* left: /data/data/( applicationId )/files/instant-run/left
* 还是
* right: /data/data/( applicationId )/files/instant-run/right
*/
public static void startUpdate() {
// Wipe the back-buffer, if already present
getWriteFolder(true);
}
/**
* 进行 反转 left or right 文件夹
*
* 目前,所有调用 finishUpdate(boolean wroteResources) 的
* 地方,都传了 true。所以,一定会进行反转 left or right 文件夹
*
* @param wroteResources wroteResources
*/
public static void finishUpdate(boolean wroteResources) {
if (wroteResources) {
swapFolders();
}
}
/**
* 生成 dex( 主要用于 冷部署 )
*
* 1. 校验目录:/data/data/( applicationId )/files/instant-run/dex。没有,则创建
* 2. 通过调用 writeRawBytes 方法,在该目录下保存 dex 文件
*
* @param bytes 二进制数据
* @param name dex 名
* @return File
*/
@Nullable
public static File writeDexShard(@NonNull byte[] bytes, @NonNull String name) {
File dexFolder = getDexFileFolder(getDataFolder(), true);
if (dexFolder == null) {
return null;
}
File file = new File(dexFolder, name);
writeRawBytes(file, bytes);
return file;
}
/**
* 生成资源文件 resources 或者 resources.ap_ ( 主要用于 温部署 )
*
* 路径一般为 /data/data/( applicationId )/files/instant-run/left( right )/resources.ap_( resources
* )
*
* 1. 拿到以上路径后,创建该路径的父文件夹
* 2. 生成资源文件:
* - 2.1 如果生成 resources.ap_:
* - 2.1.1 如果 USE_EXTRACTED_RESOURCES = true,那么该流为 instant-run.zip 的数据,直接复制
* - 出内部的 dex 到 /data/data/( applicationId )/files/instant-run/left( right ) 目
* - 录下
* - 2.1.2 如果 USE_EXTRACTED_RESOURCES = false,生成
* - /data/data/( applicationId )/files/instant-run/left( right )/resources.ap_
* - 2.2 如果生成 resources,那么直接写出
* - /data/data/( applicationId )/files/instant-run/left( right )/resources
*
* @param relativePath 文件名
* @param bytes instant-run.zip 流 or 资源流
*/
public static void writeAaptResources(@NonNull String relativePath, @NonNull byte[] bytes) {
// TODO: Take relativePath into account for the actual destination file
File resourceFile = getResourceFile(getWriteFolder(false));
File file = resourceFile;
if (USE_EXTRACTED_RESOURCES) {
file = new File(file, relativePath);
}
File folder = file.getParentFile();
if (!folder.isDirectory()) {
boolean created = folder.mkdirs();
if (!created) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Cannot create local resource file directory " + folder);
}
return;
}
}
if (relativePath.equals(RESOURCE_FILE_NAME)) {
//noinspection ConstantConditions
if (USE_EXTRACTED_RESOURCES) {
extractZip(resourceFile, bytes);
} else {
writeRawBytes(file, bytes);
}
} else {
writeRawBytes(file, bytes);
}
}
/**
* 在 dex-temp 文件夹下 生成 dex ( 主要用于 热部署 )
*
* @param bytes 二进制数据
* @return dex 文件的路径
*/
@Nullable
public static String writeTempDexFile(byte[] bytes) {
File file = getTempDexFile();
if (file != null) {
writeRawBytes(file, bytes);
return file.getPath();
} else {
Log.e(LOG_TAG, "No file to write temp dex content to");
}
return null;
}
/**
* 获取 dex-temp 文件夹下,最近修改的.dex 文件的更新时间
*
* 获取 /data/data/( applicationId )/files/instant-run/dex-temp 文件夹下,最近修改的
* .dex 文件的更新时间
* 如果 /data/data/( applicationId )/files/instant-run/dex-temp 文件夹 不存在 或者
* 文件夹内没有文件 return 0L
*
* @param dataFolder /data/data/( applicationId )/files/instant-run
* @return 最新 .dex 的时间
*/
/**
* Returns the modification time of the newest hotswap (reload) dex file
* or 0 if there are no hotswap dex files in the passed dataFolder
*/
public static long getMostRecentTempDexTime(@NonNull File dataFolder) {
File dexFolder = getTempDexFileFolder(dataFolder);
if (!dexFolder.isDirectory()) {
return 0L;
}
File[] files = dexFolder.listFiles();
if (files == null) {
return 0L;
}
long newest = 0L;
for (File file : files) {
if (file.getPath().endsWith(CLASSES_DEX_SUFFIX)) {
newest = Math.max(newest, file.lastModified());
}
}
return newest;
}
/**
* 清空 dex-temp 下的 .dex 文件 ( 用于清空 热部署 产生的 dex-temp 文件夹中的 dex )
*
* 清空 /data/data/( applicationId )/files/instant-run/dex-temp 文件夹下的 .dex 文件
* 但是,保留 dex-temp 文件夹
*
* 同时记录 sHavePurgedTempDexFolder = true
*
* @param dataFolder /data/data/( applicationId )/files/instant-run
*/
/**
* Removes .dex files from the temp dex file folder
*/
public static void purgeTempDexFiles(@NonNull File dataFolder) {
sHavePurgedTempDexFolder = true;
File dexFolder = getTempDexFileFolder(dataFolder);
if (!dexFolder.isDirectory()) {
return;
}
File[] files = dexFolder.listFiles();
if (files == null) {
return;
}
for (File file : files) {
if (file.getPath().endsWith(CLASSES_DEX_SUFFIX)) {
boolean deleted = file.delete();
if (!deleted) {
Log.e(LOG_TAG, "Could not delete temp dex file " + file);
}
}
}
}
/**
* 校验 resources.ap_ 的大小
*
* 前提:文件得是 resources.ap_
*
* @param path resources.ap_
* @return 路径是 resources.ap_ 的话,返回 resources.ap_ 的大小
* 不是的话,返回 -1
*/
public static long getFileSize(@NonNull String path) {
// Currently only handle this for resource files
if (path.equals(RESOURCE_FILE_NAME)) {
File file = getExternalResourceFile();
if (file != null) {
return file.length();
}
}
return -1;
}
/**
* 获取 resources.ap_ 文件的 MD5 值
*
* @param path resources.ap_
* @return 路径是 resources.ap_ 的话,返回 resources.ap_ 的 MD5 值
* 不是的话,返回 null
*/
@Nullable
public static byte[] getCheckSum(@NonNull String path) {
// Currently only handle this for resource files
if (path.equals(RESOURCE_FILE_NAME)) {
File file = getExternalResourceFile();
if (file != null) {
return getCheckSum(file);
}
}
return null;
}
/**
* 获取 文件的 MD5 值
* 主要获取 获取 resources.ap_ 文件的 MD5 值
*
* Computes a checksum of a file.
*
* @param file the file to compute the fingerprint for
* @return a fingerprint
*/
@Nullable
public static byte[] getCheckSum(@NonNull File file) {
try {
// Create MD5 Hash
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] buffer = new byte[4096];
BufferedInputStream input = new BufferedInputStream(new FileInputStream(file));
try {
while (true) {
int read = input.read(buffer);
if (read == -1) {
break;
}
digest.update(buffer, 0, read);
}
return digest.digest();
} finally {
input.close();
}
} catch (NoSuchAlgorithmException e) {
if (Log.isLoggable(LOG_TAG, Log.ERROR)) {
Log.e(LOG_TAG, "Couldn't look up message digest", e);
}
} catch (IOException ioe) {
if (Log.isLoggable(LOG_TAG, Log.ERROR)) {
Log.e(LOG_TAG, "Failed to read file " + file, ioe);
}
} catch (Throwable t) {
if (Log.isLoggable(LOG_TAG, Log.ERROR)) {
Log.e(LOG_TAG, "Unexpected checksum exception", t);
}
}
return null;
}
/**
* 读取 inbox/resources.ap_ ( 主要用于创建资源时,读取 inbox/resources.ap_ )
*
* 1. 路径为: /data/data/( applicationId )/files/instant-run/inbox/resources.ap_
* 2. 为了复制到 /data/data/.../files/instant-run/left(or right)/resources.ap_
*
* @param source /data/data/( applicationId )/files/instant-run/inbox/resources.ap_
* @return 资源二进制数据
*/
public static byte[] readRawBytes(@NonNull File source) {
try {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Reading the bytes for file " + source);
}
long length = source.length();
if (length > Integer.MAX_VALUE) {
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "File too large (" + length + ")");
}
return null;
}
byte[] result = new byte[(int) length];
BufferedInputStream input = new BufferedInputStream(new FileInputStream(source));
try {
int index = 0;
int remaining = result.length - index;
while (remaining > 0) {
int numRead = input.read(result, index, remaining);
if (numRead == -1) {
break;
}
index += numRead;
remaining -= numRead;
}
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "Returning length " + result.length + " for file " + source);
}
return result;
} finally {
input.close();
}
} catch (IOException ioe) {
if (Log.isLoggable(LOG_TAG, Log.ERROR)) {
Log.e(LOG_TAG, "Failed to read file " + source, ioe);
}
}
if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
Log.v(LOG_TAG, "I/O error, no bytes returned for " + source);
}
return null;
}
}