第一部分 类加载机制
一个Activity是如何被Android虚拟机找到的?
在之前的文章
红橙Darren视频笔记 自定义View总集篇(https://blog.csdn.net/u011109881/article/details/113273632)
中 有涉及一点
以ActivityThread.java中的main函数为起点,
其中调用了Looper.loop(); loop方法内部会分发消息dispatchMessage
之后进入handleMessage 走到LAUNCH_ACTIVITY的case
–> handleLaunchActivity(r, null, “LAUNCH_ACTIVITY”);
其中有非常重要的一步
Activity a = performLaunchActivity(r, customIntent);
我们这次重点是看一下这个activity是如何通过ClassLoader得到的
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
//通过ClassLoader创建activity对象
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
String pkg = intent != null && intent.getComponent() != null
? intent.getComponent().getPackageName() : null;
return getFactory(pkg).instantiateActivity(cl, className, intent);
}
public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className,
@Nullable Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (Activity) cl.loadClass(className).newInstance();//通过classLoader+反射创建Activity
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
追踪到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;
}
那么这个findClass的具体实现是什么呢 我们发现classLoader是个空实现 那么必然是ClassLoader的实现类去实现该方法的下面开始追寻他的实现类
往回走到performLaunchActivity的方法内部
java.lang.ClassLoader cl = appContext.getClassLoader();
appContext是ContextImpl实例 查看对应getClassLoader方法
ContextImpl.java
public ClassLoader getClassLoader() {
return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader());
}
看mPackageInfo是什么对象
final @NonNull LoadedApk mPackageInfo;
我们看LoadedApk 的getClassLoader方法
@UnsupportedAppUsage
public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader == null) {
createOrUpdateClassLoaderLocked(null /*addedPaths*/);
}
return mClassLoader;
}
}
这个mClassLoader是怎么初始化的呢
通过断点的方式知道它在构造方法中就初始化了
LoadedApk(ActivityThread activityThread) {
mActivityThread = activityThread;
mApplicationInfo = new ApplicationInfo();
mApplicationInfo.packageName = "android";
mPackageName = "android";
mAppDir = null;
mResDir = null;
mSplitAppDirs = null;
mSplitResDirs = null;
mSplitClassLoaderNames = null;
mOverlayDirs = null;
mDataDir = null;
mDataDirFile = null;
mDeviceProtectedDataDirFile = null;
mCredentialProtectedDataDirFile = null;
mLibDir = null;
mBaseClassLoader = null;
mSecurityViolation = false;
mIncludeCode = true;
mRegisterPackage = false;
mClassLoader = ClassLoader.getSystemClassLoader();//这里初始化
mResources = Resources.getSystem();
mAppComponentFactory = createAppFactory(mApplicationInfo, mClassLoader);
}
继续往下跟
public static ClassLoader getSystemClassLoader() {
return SystemClassLoader.loader;
}
static private class SystemClassLoader {
public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}
private static ClassLoader createSystemClassLoader() {
String classPath = System.getProperty("java.class.path", ".");
String librarySearchPath = System.getProperty("java.library.path", "");
// String[] paths = classPath.split(":");
// URL[] urls = new URL[paths.length];
// for (int i = 0; i < paths.length; i++) {
// try {
// urls[i] = new URL("file://" + paths[i]);
// }
// catch (Exception ex) {
// ex.printStackTrace();
// }
// }
//
// return new java.net.URLClassLoader(urls, null);
// TODO Make this a java.net.URLClassLoader once we have those?
//最终发现 是一个PathClassLoader
return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
我们发现该类和其的父类BaseDexClassLoader了,但是我们都看不到源码,显然ClassLoader中的findClass是定义在父类中了,我们必须要看到完整的安卓源码才知道实现方式
这里有一个在线网站 可以查找完整Android源码,不过貌似只更新到9.0 http://androidxref.com/
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);//here
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;
}
33 /**
34 * Hook for customizing how dex files loads are reported.
35 *
36 * This enables the framework to monitor the use of dex files. The
37 * goal is to simplify the mechanism for optimizing foreign dex files and
38 * enable further optimizations of secondary dex files.
39 *
40 * The reporting happens only when new instances of BaseDexClassLoader
41 * are constructed and will be active only after this field is set with
42 * {@link BaseDexClassLoader#setReporter}.
43 */
44 /* @NonNull */ private static volatile Reporter reporter = null;
45
46 private final DexPathList pathList;
这个DexPathList又是个什么呢
其路径位于
/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
我们需要关注他的两参数的findClass方法
472 /**
473 * Finds the named class in one of the dex files pointed at by
474 * this instance. This will find the one in the earliest listed
475 * path element. If the class is found but has not yet been
476 * defined, then this method will define it in the defining
477 * context that this instance was constructed with.
478 *
479 * @param name of class to find
480 * @param suppressed exceptions encountered whilst finding the class
481 * @return the named class or {@code null} if the class is not
482 * found in any of the dex files
483 */
484 public Class<?> findClass(String name, List<Throwable> suppressed) {
485 for (Element element : dexElements) {
486 Class<?> clazz = element.findClass(name, definingContext, suppressed);
487 if (clazz != null) {
488 return clazz;
489 }
490 }
491
492 if (dexElementsSuppressedExceptions != null) {
493 suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
494 }
495 return null;
496 }
59 /**
60 * List of dex/resource (class path) elements.
61 * Should be called pathElements, but the Facebook app uses reflection
62 * to modify 'dexElements' (http://b/7726934).
63 */
64 private Element[] dexElements;
现在我们找到最终的findClass调用的地方了
小结一下流程:
PathClassLoader继承自BaseDexClassLoader继承自ClassLoader
ActivityThread.handleLaunchActivity(r, null, “LAUNCH_ACTIVITY”);–>
Activity a = performLaunchActivity(r, customIntent);–>
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);–>
getFactory(pkg).instantiateActivity(cl, className, intent);–>
(Activity) cl.loadClass(className).newInstance–>
ClassLoader.loadClass–>
ClassLoader.findClass–>
BaseDexClassLoader.findClass–>
DexPathList.findClass
第二部分 自己写个热修复
dex热修复的2种方案
1.可以把出错的class 重新打成一个dex包,混淆可能影响该方案。
2.直接下载整个dex包,然后进行插入修复,但是dex包可能比较大
我这里使用第二种方案
原理:通过上面的分析 我们发现Android设备是通过Activity的name查找dex列表来加载不同的Activity的 那么如果我们改写了Android的dex列表,我们就可以实现热修复(使用修复好的Activity的dex包替换存在bug的dex包)
比如DexPathList中存放了dex列表dexElements,它遍历的时候从前向后遍历 那么我们在这个列表初始位置插入正确的dex包,class Loader找到Activity之后就不会继续向后查找存在bug的dex了
下面进入实战阶段
1.获取Android系统中原来的dex list
利用反射获取private Element[] dexElements;这个filed
2.准备dex解压路径 解压路径是拷贝路径的一个子文件夹
拷贝路径:/data/user/0/com.example.learneassyjoke/app_odex/
解压路径:/data/user/0/com.example.learneassyjoke/app_odex/odex/
3.查看是否有dex文件 如果存在 copy到指定目录,只有在app内部的目录才能解压和解析dex文件(这里code有点问题 如果不存在 不应该创建文件)
4.获取copy目录中的所有dex补丁包(*.dex)
5.合并所有补丁dex包以及运行中的dex包
6.将合并的dex包注入classLoader
主体类
public class FixDexManager {
private final Context mContext;
private final File mDexDir;//copy路径 /data/user/0/com.example.learneassyjoke/app_odex/
private final static String TAG = "FixDexManager";
public FixDexManager(Context context) {
this.mContext = context;
// 获取应用可以访问的dex目录
this.mDexDir = context.getDir("odex", Context.MODE_PRIVATE);
}
public void fixBugByDex() throws Exception {
//1.获取Android系统中原来的dex list
Object applicationOriginDexElements = loadOriginDex();
Log.e(TAG, "fixBugByDex: loadOriginDex end ");
//2.准备dex解压路径 解压路径是拷贝路径的一个子文件夹
File unzipDirectory = prepareUnzipDir();
Log.e(TAG, "fixBugByDex: prepareUnzipDir end " + unzipDirectory);
//3.查看是否有dex文件 如果存在 copy到指定目录,只有在app内部的目录才能解压和解析dex文件
File srcFile = findDexFiles();
File destFile = new File(mDexDir,srcFile.getName());
copyFile(srcFile, destFile);
//4.获取copy目录中的所有dex补丁包(*.dex)
List<File> allFixDexFile = loadAllFixDex();
if (allFixDexFile == null || allFixDexFile.size() == 0) {
Log.e(TAG, "fixBugByDex: dex size incorrect, return!");
return;
} else {
Log.e(TAG, "fixBugByDex: dex size is " + allFixDexFile.size());
}
//5.合并所有补丁dex包以及运行中的dex包
applicationOriginDexElements = combineDex(allFixDexFile, unzipDirectory, applicationOriginDexElements);
Log.e(TAG, "fixBugByDex: combineDex end");
//6.将合并的dex包注入classLoader
injectDexToClassLoader(applicationOriginDexElements);
Log.e(TAG, "fixBugByDex: injectDexToClassLoader end");
}
/**
* copy file
*
* @param src source file
* @param dest target file
* @throws IOException
*/
public static void copyFile(File src, File dest) throws IOException {
FileChannel inFile = null;
FileChannel outFile = null;
try {
if (!dest.exists()) {
dest.createNewFile();
}
inFile = new FileInputStream(src).getChannel();
outFile = new FileOutputStream(dest).getChannel();
inFile.transferTo(0, inFile.size(), outFile);
} finally {
if (inFile != null) {
inFile.close();
}
if (outFile != null) {
outFile.close();
}
}
}
private File findDexFiles() throws Exception {
File fixFile = new File(mContext.getExternalFilesDir(null), "fix2.dex");
if (fixFile.exists()) {
Log.e(TAG, "findDexFiles: " + fixFile.getAbsolutePath());
} else {
boolean b = fixFile.mkdir();
if (b) {
Log.e(TAG, "create success" + fixFile.getAbsolutePath());
} else {
Log.e(TAG, "create failed" + fixFile.getAbsolutePath());
throw new FileNotFoundException();
}
}
return fixFile;
}
private File prepareUnzipDir() {
File unzipDirectory = new File(mDexDir, "odex");
if (!unzipDirectory.exists()) {
boolean res = unzipDirectory.mkdirs();
if (!res) {
Log.e(TAG, "prepareUnzipDir: mkdir failed");
}
}
return unzipDirectory;
}
private void injectDexToClassLoader(Object applicationOriginDexElements) throws Exception {
// 1.先获取 pathList
ClassLoader applicationClassLoader = mContext.getClassLoader();
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathList = pathListField.get(applicationClassLoader);
// 2. pathList里面的dexElements
Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
dexElementsField.set(pathList, applicationOriginDexElements);
}
private Object combineDex(List<File> allFixDexFiles, File unzipDirectory, Object applicationOriginDexElements) throws Exception {
ClassLoader applicationClassLoader = mContext.getClassLoader();
for (File fixDexFile : allFixDexFiles) {
// dexPath dex路径
// optimizedDirectory 解压路径
// libraryPath .so文件位置
// parent 父ClassLoader
ClassLoader fixDexClassLoader = new BaseDexClassLoader(
fixDexFile.getAbsolutePath(),// dex路径 必须要在应用目录下的odex文件中
unzipDirectory,// 解压路径
null,// .so文件位置
applicationClassLoader // 父ClassLoader
);
//5.1 解析当前fixDexFile为Elements对象
Object fixDexElements = getDexElementsByClassLoader(fixDexClassLoader);
//5.2 把补丁的dexElement 插到 已经运行的 dexElement 的最前面
//即在applicationClassLoader数组前插入数次解析出的fixDexElements对象
//核心是数组合并
applicationOriginDexElements = combineArray(fixDexElements, applicationOriginDexElements);
}
return applicationOriginDexElements;
}
/**
* 从classLoader中获取 dexElements
*/
private Object getDexElementsByClassLoader(ClassLoader classLoader) throws Exception {
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathList = pathListField.get(classLoader);
Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
return dexElementsField.get(pathList);
}
/**
* 合并两个数组
*/
private static Object combineArray(Object arrayHead, Object arrayTail) {
Class<?> localClass = arrayHead.getClass().getComponentType();
int i = Array.getLength(arrayHead);
int j = i + Array.getLength(arrayTail);
Log.d(TAG, "combineArray: first array length "+i+" second array length "+j);
Object result = Array.newInstance(localClass, j);
for (int k = 0; k < j; ++k) {
if (k < i) {
Array.set(result, k, Array.get(arrayHead, k));
} else {
Array.set(result, k, Array.get(arrayTail, k - i));
}
}
return result;
}
private List<File> loadAllFixDex() {
File[] dexFiles = mDexDir.listFiles();
if (dexFiles == null || dexFiles.length == 0) {
Log.e(TAG, "file list is null or file size is 0");
return null;
}
List<File> fixDexFiles = new ArrayList<>();
for (File dexFile : dexFiles) {
if (dexFile.getName().endsWith(".dex")) {
fixDexFiles.add(dexFile);
}
}
return fixDexFiles;
}
//TODO 可以和getDexElementsByClassLoader合并
private Object loadOriginDex() throws Exception {
ClassLoader applicationClassLoader = mContext.getClassLoader();
// 1.1 先获取 pathList
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathList = pathListField.get(applicationClassLoader);
if (pathList == null) {
return null;
}
// 1.2 pathList里面的dexElements
Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
return dexElementsField.get(pathList);
}
}
在application的onCreate中调用fixbug的方法
//自己写的dex fix热修复
private void fixDexBug() {
//准备工作 将修复好的dex 拷贝到 /storage/emulated/0/Android/data/app包名/files/
FixDexManager fixDexManager = new FixDexManager(this);
try {
fixDexManager.fixBugByDex();
Log.e(TAG, "fixDexBug: 修复成功");
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "fixDexBug: 修复失败");
}
}
开始测试
准备工作,在Activity中写一个存在问题的界面或者bug
比如我是这么写的
mTextView.setText("This is Bug text ");
运行该程序
之后将该bug修改
mTextView.setText("This is OK text ");
生成apk 解压,这里我发现Android28和29会自动开启multiDexEnabled因此它生成了4个dex文件
那么mainActivity在哪个dex文件呢 这里涉及一点反编译了
下载最新的dex2jar工具包先将dex解析成jar
https://github.com/pxb1988/dex2jar/releases
然后用工具jd-gui查看jar文件 这里正确的情况应该是OK text,我用存在bug的apk做了个测试
工具应该可以从这里下载 我是很早以前下的 不记得从哪里下得了
http://java-decompiler.github.io/
找到存在正确Activity的类的dex文件 将之重命名为fix2.dex (我的程序中是这么写的),copy到/storage/emulated/0/Android/data/app包名/files/
之后重新启动程序,问题即可热修复成功
当然如果不想进行反编译,应该也可以禁止分包 ,即在gradle中设置multiDexEnabled false
然而生成的dex文件虽然只有一个了 但是我反编译后没有找到MainActivity,热修复也没有成功,不清楚问题出在哪里,哪位大神知道原因还望指教。
后记
实际上热修复的代码我早就写的差不多了,但是始终没有测试成功,没有报错没有crash,后面反编译+log+断点调试都没有找到原因,最后我无意间将fixDexBug的调用从MainActivity移动到了Application中就好了。本来我都快放弃了,没成想瞎蒙解决了问题,这里在修复的Activity调用fix方法确实晚了,但是我当时没有考虑过这个问题,一直在FixDexManager调试,发现即使到了注入DexElement的地方都没有发现问题,搞了三个晚上心态都快崩了。这大概就是有心栽花花不开 无心插柳柳成荫,有的时候运气也很重要呀(狗头)