使用到的技術(shù)點(diǎn)
1霹抛、Java類加載機(jī)制;
2卷谈、Android加載dex文件杯拐;
3、反射;原理:
用修復(fù)好的類替換有問(wèn)題的類端逼。在App重新啟動(dòng)后讓Classloader去加載新的類朗兵。因?yàn)樵贏pp運(yùn)行到一半的時(shí)候,所有需要發(fā)生變更的類已經(jīng)被加載過(guò)了顶滩,在Android上是無(wú)法對(duì)一個(gè)類進(jìn)行卸載的余掖。如果不重啟,
原來(lái)的類還在虛擬機(jī)中礁鲁,就無(wú)法加載新類
盐欺。因此,只有在下次重啟的時(shí)候仅醇,在還沒(méi)有走到業(yè)務(wù)邏輯之前搶先加載補(bǔ)丁中的新類冗美,這樣后續(xù)訪問(wèn)這個(gè)類時(shí),就會(huì)Resolve為新類析二。從而達(dá)到熱修復(fù)的目的粉洼。關(guān)鍵問(wèn)題是:
如何替換一個(gè)類?
這里沒(méi)有直接替換一個(gè)類叶摄,而是通過(guò)類加載機(jī)制属韧,在加載問(wèn)題類之前,先加載補(bǔ)丁類的方案達(dá)到替換的目的蛤吓。具體操作的依據(jù)挫剑,首先因?yàn)橥粋€(gè)類加載器在嘗試加載一個(gè)類的時(shí)候,會(huì)先判斷這個(gè)類是否已經(jīng)加載柱衔,如果已經(jīng)加載則不會(huì)再次去加載樊破;其次,Android加載dex時(shí)唆铐,會(huì)按照先后順序依次加載的
哲戚。
一、Java加載機(jī)制
ClassLoader#loadClass()方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
(1)艾岂、從當(dāng)前類加載器的已加載類緩存中根據(jù)類的全路徑名查詢是否存在該類顺少,如果存在則直接返回。
(2)王浴、如果當(dāng)前類存在父類加載器
脆炎,則調(diào)用父類加載器的loadClass(name,false)方法對(duì)其進(jìn)行加載氓辣。
(3)秒裕、如果當(dāng)前類加載器不存在父類加載器,則直接調(diào)用啟動(dòng)類加載器
對(duì)該類進(jìn)行加載钞啸。
(4)几蜻、如果當(dāng)前類的所有父類加載器都沒(méi)有成功加載class喇潘,則嘗試調(diào)用當(dāng)前類加載器的findClass方法對(duì)其進(jìn)行加載,該方法就是我們自定義加載器需要重寫的方法
梭稚。
(5)颖低、最后如果類被成功加載,則做一些性能數(shù)據(jù)的統(tǒng)計(jì)弧烤。
(6)忱屑、由于loadClass指定了resolve為false,所以不會(huì)進(jìn)行連接階段的繼續(xù)執(zhí)行暇昂,這也就解釋了為什么通過(guò)類加載器加載類并不會(huì)導(dǎo)致類的初始化莺戒。
二、Android類加載的機(jī)制
Android中相關(guān)的類加載器PathClassLoader
和DexClassLoader
话浇。
(1)、DexClassLoader:能夠加載
自定義的jar/apk/dex
(2)闹究、PathClassLoader:只能加載系統(tǒng)中已經(jīng)安裝過(guò)的apk
所以Android系統(tǒng)默認(rèn)的類加載器為PathClassLoader
幔崖,而DexClassLoader可以像JVM的ClassLoader一樣提供動(dòng)態(tài)加載。
PathClassLoader和DexClassLoader加載Class的相關(guān)源碼:
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
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;
}
}
DexPathList #findClass()
final class DexPathList {
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private Element[] dexElements;
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
}
DexFile#loadClassBinaryName()
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List<Throwable> suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
從上面的源碼可以得知渣淤,當(dāng)Android中類加載器嘗試加載類的時(shí)候赏寇,會(huì)調(diào)用DexPathList
的findClass()
方法,通過(guò)遍歷dexElements
中的Element
价认,得到DexFile
嗅定,再通過(guò)DexFile
調(diào)用loadClassBinaryName()
方法加載類。類加載成功后用踩,就直接返回渠退。因此,可以通過(guò)將修復(fù)好的dex插入到dexElements的集合(出現(xiàn)bug的xxx.class所在的dex的前面)的位置脐彩,就可以達(dá)到間接替換bug類的目的
碎乃。
最本質(zhì)的實(shí)現(xiàn)原理:類加載器去加載某個(gè)類的時(shí)候,是去dexElements里面從頭往下查找的
惠奸。fixed.dex,classes1.dex,classes2.dex,classes3.dex
三梅誓、具體實(shí)現(xiàn)
Class BaseDexClassLoader{
private DexPathList pathList;
}
Class DexPathList{
private Element[] dexElements;
}
先獲取到已安裝App中原有的dexElements(appDexElements)和通過(guò)DexClassLoader加載已經(jīng)修復(fù)好的dex得到的dexElements(fixedDexElements)佛南。然后再將appDexElements和fixedDexElements合并成一個(gè)新的dexElements(newDexElements)梗掰。
核心類代碼
FixDexUtils
public class FixDexUtils {
private static HashSet<File> sLoadedDexFiles = new HashSet();
static {
sLoadedDexFiles.clear();
}
public static void loadFixedDex(Context context){
File fileDir = context.getDir(Constants.DEX_DIR, Context.MODE_PRIVATE);
File[] dexFiles = fileDir.listFiles();
for(File file:dexFiles){
if(file.getName().startsWith("classes") && file.getName().endsWith(".dex")){
sLoadedDexFiles.add(file);
}
}
doDexInject(context, fileDir, sLoadedDexFiles);
}
private static void doDexInject(final Context context, File filesDir, HashSet<File> loadedDexs){
String optimizedDir = filesDir.getAbsolutePath() + File.separator + Constants.OPT_DEX;
File fopt = new File(optimizedDir);
if(!fopt.exists()){
fopt.mkdirs();
}
try {
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
for(File dex:loadedDexs){
DexClassLoader dexClassLoader = new DexClassLoader(dex.getAbsolutePath(), fopt.getAbsolutePath(), null, pathClassLoader);
Object dexObj = getPathList(dexClassLoader);
Object pathObj = getPathList(pathClassLoader);
Object dexElementsList = getDexElements(dexObj);
Object pathDexElementsList = getDexElements(pathObj);
//合并dexElements
Object dexElements = combineArray(dexElementsList, pathDexElementsList);
//重新給PathList里面的Element[] dexElements賦值
Object pathList = getPathList(pathClassLoader);
setFiled(pathList, pathList.getClass(), "dexElements", dexElements);
Object finalElementsList = getDexElements(pathList);
System.out.println("--------"+finalElementsList);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void setFiled(Object obj, Class<?> clazz, String fieldName, Object value) throws Exception {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
private static Object getField(Object obj, Class<?> clazz, String fieldName) throws NoSuchFieldException, IllegalAccessException {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
}
private static Object getPathList(Object baseDexClassLoader) throws Exception {
return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
private static Object getDexElements(Object obj) throws Exception {
return getField(obj, obj.getClass(), "dexElements");
}
private static Object combineArray(Object arrLhs, Object arrRhs){
Class<?> clazz = arrLhs.getClass().getComponentType();
int i = Array.getLength(arrLhs);
int len = i + Array.getLength(arrRhs);
Object arrResult = Array.newInstance(clazz, len);
for(int k = 0; k < len; k++){
if(k < i){
Array.set(arrResult, k, Array.get(arrLhs, k));
}else{
Array.set(arrResult, k, Array.get(arrRhs, k - i));
}
}
return arrResult;
}
}
FMApplication
public class FMApplication extends Application {
@Override
public void onCreate() {
ClassLoader classLoader = getClassLoader();
System.out.println("--------onCreate---ClassLoader:"+getClassLoader());
super.onCreate();
}
@Override
protected void attachBaseContext(Context base) {
System.out.println("--------attachBaseContext");
MultiDex.install(base);
FixDexUtils.loadFixedDex(base);
super.attachBaseContext(base);
}
}
app#build.gradle的核心配置
android {
compileSdkVersion 27
defaultConfig {
...
multiDexEnabled true
...
}
buildTypes {
release {
//指定單獨(dú)打到一個(gè)dex的類
multiDexKeepFile file('dex.keep')
def myFile = file('dex.keep')
println("isFileExists:"+myFile.exists())
println "dex keep"
minifyEnabled false
}
}
}
dependencies {
...
implementation 'com.android.support:multidex:1.0.1'
...
}
四、關(guān)于FixedClass.class打包成classes.dex
dx.bat所在目錄:
sdk\build-tools\27.0.3
dx --dex --output=D:\Users\x\Desktop\dex\classes2.dex D:\Users\x\Desktop\dex
命令解釋:
--output=D:\Users\x\Desktop\dex\classes2.dex 指定輸出路徑
D:\Users\x\Desktop\dex 最后指定去打包哪個(gè)目錄下面的class字節(jié)文件(注意要包括全路徑的文件夾嗅回,也可以有多個(gè)class)
class_2_dex.png
五及穗、Java反射機(jī)制可以動(dòng)態(tài)修改實(shí)例中final修飾的成員變量嗎?
回答是分兩種情況的绵载。
- 當(dāng)final修飾的成員變量在定義的時(shí)候就初始化了值拥坛,那么java反射機(jī)制就已經(jīng)不能動(dòng)態(tài)修改它的值了蓬蝶。
- 當(dāng)final修飾的成員變量在定義的時(shí)候并沒(méi)有初始化值的話,那么就還能通過(guò)java反射機(jī)制來(lái)動(dòng)態(tài)修改它的值猜惋。