熱修復無疑是這2年較火的新技術,是做爲安卓工程師必學的技能之一。在熱修復出現以前,一個已經上線的app中若是出現了bug,即便是一個很是小的bug,不及時更新的話有可能存在風險,若要及時更新就得將app從新打包發佈到應用市場後,讓用戶再一次下載,這樣就大大下降了用戶體驗,當熱修復出現以後,這樣的問題就再也不是問題了。java
目前較火的熱修復方案大體分爲兩派,分別是:android
本篇的主題並不是講述上面兩種方案的使用,而是基於java加載機制,來研究熱修復的原理與實現。(相似tinker,固然tinker沒這麼簡單)git
關於bug的概念本身百度百科吧,我認爲的bug通常有2種(可能不太準確):github
這兩種狀況通常是一個或多個class出現了問題,在一個理想的狀態下,咱們只需將修復好的這些個class更新到用戶手機上的app中就能夠修復這些bug了。但說着簡單,要怎麼才能動態更新這些class呢?其實,無論是哪一種熱修復方案,確定是以下幾個步驟:數組
這裏的**"某種方式"**,對本篇而言,就是使用Android的類加載器,經過類加載器加載這些修復好的class,覆蓋對應有問題的class,理論上就能修復bug了。因此,下面就先來了解和分析Android中的類加載器吧。服務器
Android跟java有很大的淵源,基於jvm的java應用是經過ClassLoader來加載應用中的class的,但咱們知道Android對jvm優化過,使用的是dalvik,且class文件會被打包進一個dex文件中,底層虛擬機有所不一樣,那麼它們的類加載器固然也是會有所區別,在Android中,要加載dex文件中的class文件就須要用到 PathClassLoader 或 DexClassLoader 這兩個Android專用的類加載器。微信
通常的源碼在Android Studio中能夠查到,但 PathClassLoader 和 DexClassLoader 的源碼是屬於系統級源碼,因此沒法在Android Studio中直接查看。不過,有兩種方式能夠在外部進行查看:第一種是經過下載Android鏡像源碼的方式進行查看,但通常鏡像源碼體積較大,很差下載,並且就只是爲了看三、4個文件的源碼動不動就下載三、4個g的源碼,確實不太明智,因此咱們通常採用第二種方式:到androidxref.com這個網站上直接查看,下面會列出以後要分析的幾個類的源碼地址,供看客們方便瀏覽。markdown
如下是Android 5.0中的部分源碼:cookie
由於PathClassLoader與DexClassLoader的源碼都很簡單,我就直接將它們的所有源碼複製過來了:網絡
// PathClassLoader
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
// DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
複製代碼
經過比對,能夠得出2個結論:
經過觀察PathClassLoader與DexClassLoader的源碼咱們就能夠肯定,真正有意義的處理邏輯確定在BaseDexClassLoader中,因此下面着重分析BaseDexClassLoader源碼。
先來看看BaseDexClassLoader的構造函數都作了什麼:
public class BaseDexClassLoader extends ClassLoader {
...
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent){
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
...
}
複製代碼
***tip:**上面說到的"程序文件"這個概念是我本身定義的,由於從一個完整App的角度來講,程序文件指定的就是apk包中的classes.dex文件;但從熱修復的角度來看,程序文件指的是補丁。
由於PathClassLoader只會加載已安裝包中的dex文件,而DexClassLoader不只僅能夠加載dex文件,還能夠加載jar、apk、zip文件中的dex,咱們知道jar、apk、zip其實就是一些壓縮格式,要拿到壓縮包裏面的dex文件就須要解壓,因此,DexClassLoader在調用父類構造函數時會指定一個解壓的目錄。
不過,從Android 8.0開始,BaseDexClassLoader的構造函數邏輯發生了變化,optimizedDirectory過期,再也不生效,詳情可查看Android 8.0的BaseDexClassLoader.java源碼
類加載器確定會提供有一個方法來供外界找到它所加載到的class,該方法就是findClass(),不過在PathClassLoader和DexClassLoader源碼中都沒有重寫父類的findClass()方法,但它們的父類BaseDexClassLoader就有重寫findClass(),因此來看看BaseDexClassLoader的findClass()方法都作了哪些操做,代碼以下:
private final DexPathList pathList;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
// 實質是經過pathList的對象findClass()方法來獲取class
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
複製代碼
能夠看到,BaseDexClassLoader的findClass()方法其實是經過DexPathList對象(pathList)的findClass()方法來獲取class的,而這個DexPathList對象剛好在以前的BaseDexClassLoader構造函數中就已經被建立好了。因此,下面就來看看DexPathList類中都作了什麼。
在分析一個代碼量較多的源碼以前,咱們要明確要從這段源碼中要知道些什麼?這樣纔不會在「碼海」中迷失方向,我本身就定了2個小目標,分別是:
爲何是這2個目標?由於在BaseDexClassLoader的源碼中主要就用到了DexPathList的構造函數和findClass()方法。
private final Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
...
this.definingContext = definingContext;
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
...
}
複製代碼
這個構造函數中,保存了當前的類加載器definingContext,並調用了makeDexElements()獲得Element集合。
經過對splitDexPath(dexPath)的源碼追溯,發現該方法的做用其實就是將dexPath目錄下的全部程序文件轉變成一個File集合。並且還發現,dexPath是一個用冒號(":")做爲分隔符把多個程序文件目錄拼接起來的字符串(如:/data/dexdir1:/data/dexdir2:...)。
那接下來無疑是分析makeDexElements()方法了,由於這部分代碼比較長,我就貼出關鍵代碼,並以註釋的方式進行分析:
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
// 1.建立Element集合
ArrayList<Element> elements = new ArrayList<Element>();
// 2.遍歷全部dex文件(也多是jar、apk或zip文件)
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
...
// 若是是dex文件
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
// 若是是apk、jar、zip文件(這部分在不一樣的Android版本中,處理方式有細微差異)
} else {
zip = file;
dex = loadDexFile(file, optimizedDirectory);
}
...
// 3.將dex文件或壓縮文件包裝成Element對象,並添加到Element集合中
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
// 4.將Element集合轉成Element數組返回
return elements.toArray(new Element[elements.size()]);
}
複製代碼
在這個方法中,看到了一些眉目,整體來講,DexPathList的構造函數是將一個個的程序文件(多是dex、apk、jar、zip)封裝成一個個Element對象,最後添加到Element集合中。
其實,Android的類加載器(無論是PathClassLoader,仍是DexClassLoader),它們最後只認dex文件,而loadDexFile()是加載dex文件的核心方法,能夠從jar、apk、zip中提取出dex,但這裏先不分析了,由於第1個目標已經完成,等到後面再來分析吧。
再來看DexPathList的findClass()方法:
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
// 遍歷出一個dex文件
DexFile dex = element.dexFile;
if (dex != null) {
// 在dex文件中查找類名與name相同的類
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
複製代碼
結合DexPathList的構造函數,其實DexPathList的findClass()方法很簡單,就只是對Element數組進行遍歷,一旦找到類名與name相同的類時,就直接返回這個class,找不到則返回null。
爲何是調用DexFile的loadClassBinaryName()方法來加載class?這是由於一個Element對象對應一個dex文件,而一個dex文件則包含多個class。也就是說Element數組中存放的是一個個的dex文件,而不是class文件!!!這能夠從Element這個類的源碼和dex文件的內部結構看出。
終於進入主題了,通過對PathClassLoader、DexClassLoader、BaseDexClassLoader、DexPathList的分析,咱們知道,安卓的類加載器在加載一個類時會先從自身DexPathList對象中的Element數組中獲取(Element[] dexElements)到對應的類,以後再加載。採用的是數組遍歷的方式,不過注意,遍歷出來的是一個個的dex文件。
在for循環中,首先遍歷出來的是dex文件,而後再是從dex文件中獲取class,因此,咱們只要讓修復好的class打包成一個dex文件,放於Element數組的第一個元素,這樣就能保證獲取到的class是最新修復好的class了(固然,有bug的class也是存在的,不過是放在了Element數組的最後一個元素中,因此沒有機會被拿到而已)。
經過前面的一堆理論以後,是時候實踐一把了。
這一步根據bug的實際狀況修改代碼便可。
在修復bug以後,可使用Android Studio的Rebuild Project功能將代碼進行編譯,而後從build目錄下找到對應的class文件。
將修復好的class文件複製到其餘地方,例如桌面上的dex文件夾中。須要注意的是,在複製這個class文件時,須要把它所在的完整包目錄一塊兒複製。假設上圖中修復好的class文件是SimpleHotFixBugTest.class,則到時複製出來的目錄結構是:
要將class文件打包成dex文件,就須要用到dx指令,這個dx指令相似於java指令。咱們知道,java的指令有javac、jar等等,之因此可使用這類指令,是由於咱們有安裝過jdk,jdk爲咱們提供了java指令,相同的,dx指令也須要有程序來提供,它就在Android SDK的build-tools目錄下各個Android版本目錄之中。
dx指令的使用跟java指令的使用條件同樣,有2種選擇:
第一種方式參考java環境變量配置便可,這裏我選用第二種方式。下面咱們須要用到的命令是:
dx --dex --output=dex文件完整路徑 (空格) 要打包的完整class文件所在目錄,如:
dx --dex --output=C:\Users\Administrator\Desktop\dex\classes2.dex C:\Users\Administrator\Desktop\dex
具體操做看下圖:
在文件夾目錄的空白處,按住shift+鼠標右擊,可出現「在此處打開命令行窗口」。
根據原理,能夠作一個簡單的工具類:
/**
* @建立者 CSDN_LQR
* @描述 熱修復工具(只認後綴是dex、apk、jar、zip的補丁)
*/
public class FixDexUtils {
private static final String DEX_SUFFIX = ".dex";
private static final String APK_SUFFIX = ".apk";
private static final String JAR_SUFFIX = ".jar";
private static final String ZIP_SUFFIX = ".zip";
public static final String DEX_DIR = "odex";
private static final String OPTIMIZE_DEX_DIR = "optimize_dex";
private static HashSet<File> loadedDex = new HashSet<>();
static {
loadedDex.clear();
}
/**
* 加載補丁,使用默認目錄:data/data/包名/files/odex
*
* @param context
*/
public static void loadFixedDex(Context context) {
loadFixedDex(context, null);
}
/**
* 加載補丁
*
* @param context 上下文
* @param patchFilesDir 補丁所在目錄
*/
public static void loadFixedDex(Context context, File patchFilesDir) {
if (context == null) {
return;
}
// 遍歷全部的修復dex
File fileDir = patchFilesDir != null ? patchFilesDir : new File(context.getFilesDir(), DEX_DIR);// data/data/包名/files/odex(這個能夠任意位置)
File[] listFiles = fileDir.listFiles();
for (File file : listFiles) {
if (file.getName().startsWith("classes") &&
(file.getName().endsWith(DEX_SUFFIX)
|| file.getName().endsWith(APK_SUFFIX)
|| file.getName().endsWith(JAR_SUFFIX)
|| file.getName().endsWith(ZIP_SUFFIX))) {
loadedDex.add(file);// 存入集合
}
}
// dex合併以前的dex
doDexInject(context, loadedDex);
}
private static void doDexInject(Context appContext, HashSet<File> loadedDex) {
String optimizeDir = appContext.getFilesDir().getAbsolutePath() + File.separator + OPTIMIZE_DEX_DIR;// data/data/包名/files/optimize_dex(這個必須是本身程序下的目錄)
File fopt = new File(optimizeDir);
if (!fopt.exists()) {
fopt.mkdirs();
}
try {
// 1.加載應用程序的dex
PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
for (File dex : loadedDex) {
// 2.加載指定的修復的dex文件
DexClassLoader dexLoader = new DexClassLoader(
dex.getAbsolutePath(),// 修復好的dex(補丁)所在目錄
fopt.getAbsolutePath(),// 存放dex的解壓目錄(用於jar、zip、apk格式的補丁)
null,// 加載dex時須要的庫
pathLoader// 父類加載器
);
// 3.合併
Object dexPathList = getPathList(dexLoader);
Object pathPathList = getPathList(pathLoader);
Object leftDexElements = getDexElements(dexPathList);
Object rightDexElements = getDexElements(pathPathList);
// 合併完成
Object dexElements = combineArray(leftDexElements, rightDexElements);
// 重寫給PathList裏面的Element[] dexElements;賦值
Object pathList = getPathList(pathLoader);// 必定要從新獲取,不要用pathPathList,會報錯
setField(pathList, pathList.getClass(), "dexElements", dexElements);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 反射給對象中的屬性從新賦值
*/
private static void setField(Object obj, Class<?> cl, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cl.getDeclaredField(field);
declaredField.setAccessible(true);
declaredField.set(obj, value);
}
/**
* 反射獲得對象中的屬性值
*/
private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}
/**
* 反射獲得類加載器中的pathList對象
*/
private static Object getPathList(Object baseDexClassLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
/**
* 反射獲得pathList中的dexElements
*/
private static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException {
return getField(pathList, pathList.getClass(), "dexElements");
}
/**
* 數組合並
*/
private static Object combineArray(Object arrayLhs, Object arrayRhs) {
Class<?> componentType = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);// 獲得左數組長度(補丁數組)
int j = Array.getLength(arrayRhs);// 獲得原dex數組長度
int k = i + j;// 獲得總數組長度(補丁數組+原dex數組)
Object result = Array.newInstance(componentType, k);// 建立一個類型爲componentType,長度爲k的新數組
System.arraycopy(arrayLhs, 0, result, 0, i);
System.arraycopy(arrayRhs, 0, result, i, j);
return result;
}
}
複製代碼
代碼雖然較長,但註釋寫得很清楚,請仔細看,這裏要說兩點:
經反饋,這個是你們遇到的最多的一個問題,這裏我把注意事項和個人解決方法寫清楚:
// 合併完成
Object dexElements = combineArray(leftDexElements, rightDexElements);
// 重寫給PathList裏面的Element[] dexElements;賦值
Object pathList = getPathList(pathLoader);// 必定要從新獲取,不要用pathPathList,會報錯
setField(pathList, pathList.getClass(), "dexElements", dexElements);
複製代碼
在合併守Element數組後,必定要再從新獲取一遍App中的原有的pathList,不要複用前面的pathPathList,絕對會報錯(Class ref in pre-verified class resolved to unexpected implementation)。
Android Studio的Instant Run功能也是用到了熱修復的原理,在從新安裝app時並不會完整安裝,只會動態修改有更新的class部分,它會影響到測試結果,在跟着本文作試驗的同窗請確保Instant Run已經關閉。
我在測試的過程當中,使用的是Genymotion,發現Android 4.4的模擬器一直沒法打上補丁,可是Android 5.0的模擬器卻能夠,真機測試也沒問題,因此建議不要使用Android 5.0如下的模擬器來測試,強烈建議用真機測試!!
DexClassLoader dexLoader = new DexClassLoader(
dex.getAbsolutePath(),// 修復好的dex(補丁)所在目錄
fopt.getAbsolutePath(),// 存放dex的解壓目錄(用於jar、zip、apk格式的補丁)
null,// 加載dex時須要的庫
pathLoader// 父類加載器
複製代碼
上面的代碼是建立一個DexClassLoader對象,其中第1個和第2個參數有個細節須要注意:
若是你把optimizedDirectory指定成SD卡目錄,則會報以下錯誤:
java.lang.IllegalArgumentException: Optimized data directory /storage/emulated/0/opt_dex is not owned by the current user. Shared storage cannot protect your application from code injection attacks.
意思是說SD卡目錄不屬於當前用戶。此外,這裏再校訂以前的一個小問題,optimizedDirectory不只僅存放從壓縮包出來的dex文件,若是補丁文件就是一個dex文件,那麼它也會將這個補丁文件複製到optimizedDirectory目錄下。
前面已經說了不少次DexClassLoader能夠加載jar、apk、zip格式補丁文件了,那這類格式的補丁文件有什麼要求嗎?
答案是:這類壓縮包中必須放着一個dex文件,並且對名字有要求,必須是classes.dex。Why?這就須要分析DexPathList類中的loadDexFile()方法了。
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
// 若是optimizedDirectory爲null,其實就是PathClassLoader加載dex文件的處理方式
if (optimizedDirectory == null) {
return new DexFile(file);
}
// 若是optimizedDirectory不是null,這就是DexClassLoader加載dex文件的處理方式了,重點看這個
else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
複製代碼
參數一file,多是dex文件,也多是jar、apk、zip文件。
從上面的源碼中,不難看出else分支纔是DexClassLoader加載dex文件的處理方式,它調用的是optimizedPathFor()方法拿到以後dex文件在optimizedDirectory目錄下的全路徑:
private static String optimizedPathFor(File path, File optimizedDirectory) {
String fileName = path.getName();
if (!fileName.endsWith(DEX_SUFFIX)) {
int lastDot = fileName.lastIndexOf(".");
// 若是補丁沒有後綴,就給它加一個".dex"後綴
if (lastDot < 0) {
fileName += DEX_SUFFIX;
}
// 無論補丁後綴是dex、jar、apk仍是zip,最終放到optimizedDirectory目錄下的必定是dex文件
else {
StringBuilder sb = new StringBuilder(lastDot + 4);
sb.append(fileName, 0, lastDot);
sb.append(DEX_SUFFIX);
fileName = sb.toString();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}
複製代碼
前面已經說過了,Android的類加載器最終只認dex文件,即便補丁是jar、apk、zip等壓縮文件,它也會把其中的dex文件解壓出來,因此該方法獲得的文件名必定是以dex結尾的。好了,這個optimizedPathFor()方法並非重點,回頭看loadDexFile()中的else分支還有一個DexFile.loadDex()方法,這個方法就至關重要了。
static public DexFile loadDex(String sourcePathName, String outputPathName, int flags) throws IOException {
return new DexFile(sourcePathName, outputPathName, flags);
}
複製代碼
這個方法中就調用了一下本身的構造函數,並傳入各個參數,接着來看看DexFile的構造函數:
/**
* Open a DEX file, specifying the file in which the optimized DEX
* data should be written. If the optimized form exists and appears
* to be current, it will be used; if not, the VM will attempt to
* regenerate it.
*
* This is intended for use by applications that wish to download
* and execute DEX files outside the usual application installation
* mechanism. This function should not be called directly by an
* application; instead, use a class loader such as
* dalvik.system.DexClassLoader.
*
* @param sourcePathName
* Jar or APK file with "classes.dex". (May expand this to include
* "raw DEX" in the future.)
* @param outputPathName
* File that will hold the optimized form of the DEX data.
* @param flags
* Enable optional features. (Currently none defined.)
* @return
* A new or previously-opened DexFile.
* @throws IOException
* If unable to open the source or output file.
*/
private DexFile(String sourceName, String outputName, int flags) throws IOException {
if (outputName != null) {
try {
String parent = new File(outputName).getParent();
if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
throw new IllegalArgumentException("Optimized data directory " + parent
+ " is not owned by the current user. Shared storage cannot protect"
+ " your application from code injection attacks.");
}
} catch (ErrnoException ignored) {
// assume we'll fail with a more contextual error later
}
}
mCookie = openDexFile(sourceName, outputName, flags);
mFileName = sourceName;
guard.open("close");
//System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
}
複製代碼
奇怪嗎,此次我沒有把構造函數的註釋去掉,緣由是在它的註釋中就已經有咱們想要的答案了:
@param sourcePathName Jar or APK file with "classes.dex". (May expand this to include "raw DEX" in the future.)
複製代碼
這名註釋的意思就是說,jar或apk格式的補丁文件中須要有一個classes.dex。至此,對於壓縮格式的補丁文件的要求就弄明白了。那麼接下來就只須要生成這幾種格式的補丁試一試就行了。製做這類壓縮文件也很簡單,直接用壓縮軟件壓縮成zip文件,而後改下後綴就能夠。
這部分其實本不想寫的,由於比較簡單,但想了想不寫又以爲不完整,那接下來就來測試一波吧。
佈局文件就倆按鈕,很簡單就不貼布局文件代碼了,看這兩個按鈕的點擊事件就行。
public class SimpleHotFixActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_simple_hot_fix);
}
// 「修復」按鈕的點擊事件
public void fix(View view) {
FixDexUtils.loadFixedDex(this, Environment.getExternalStorageDirectory());
}
// 「計算」按鈕的點擊事件
public void clac(View view) {
SimpleHotFixBugTest test = new SimpleHotFixBugTest();
test.getBug(this);
}
}
複製代碼
能夠看到,「修復」按鈕的點擊事件是去加載SD卡目錄下的補丁文件。
public class SimpleHotFixBugTest {
public void getBug(Context context) {
int i = 10;
int a = 0;
Toast.makeText(context, "Hello,I am CSDN_LQR:" + i / a, Toast.LENGTH_SHORT).show();
}
}
複製代碼
會發生什麼事呢?除數是0異常,一個簡單的運行時異常,修復它也很簡單,把a的值改成非0便可。
很少說,看操做。
妥妥的ArithmeticException。
Caused by: java.lang.ArithmeticException: divide by zero
首先,我將補丁文件classes2.dex放到手機的SD目錄下。
而後先點擊修復按鈕,再點計算按鈕。
大功告成,壓縮格式的補丁跟dex格式的補丁同樣,直接丟掉SD卡目錄下就好了,但必定要注意,壓縮格式的補丁中的文件必定是classes.dex!!!
權限申請:本文的提供的Demo是讀取SD卡下的補丁文件,但卻沒有爲Android6.0以上適配動態權限申請,若是你有使用該demo進行測試,那要注意本身測試機的Android版本,如果6.0以上,請務必先爲Demo分配SD卡讀寫操做權限,不然App崩潰都不知道是否是由於bug形成的 ,切記。