构造方法中初始化了pathList, 传入三个参数 , 分别为dexPath:目标文件路径(一般是dex文件,也可以是jar/apk/zip文件)所在目录。热修复时用来指定新的dexoptimizedDirectory:dex文件的输出目录(因为在加载jar/apk/zip等压缩格式的程序文件时会解压出其中的dex文件,该目录就是专门用于存放这些被解压出来的dex文件的)。libraryPath:加载程序文件时需要用到的库路径。parent:父加载器
1.2 加载类的过程
在BaseDexClassLoader中 , 紧接着构造函数的是一个叫findClass的方法 , 这个方法用来加载dex文件中对应的class文件.
@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException { List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); //从pathList中找到相应类名的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;}
大体上不难理解, 拿到初始化完成的 pathList 之后 , 根据类名找出相应的class字节码文件, 如果没有异常直接返回class.接下来我们继续跟进pathList
1.3 DexPathList
DexPathList 源码在这里好了, 点开源码不要慌 , 我们目前只需要知道两个东西:
构造函数. 我们在BaseDexClassLoader中实例化DexPathList需要用到findClass方法, 在BaseDexClassLoader的findClass中, 本质调用了DexpathList的fndClass方法.其他的方法姑且不用关心.1->构造函数
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) { this.definingContext = definingContext; ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); // save dexPath for BaseDexClassLoader this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions); this.nativeLibraryDirectories = splitPaths(libraryPath, false); this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true); List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories); allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories); this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null, suppressedExceptions); }
首先 , 将传入的classLoader保存起来 , 接下来使用makePathElements方法 ,来初始化Element数组 .
那接下来无疑是分析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个目标已经完成,等到后面再来分析吧。
2->findClass方法
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;}
在DexPathList的构造函数中已经初始化了dexElements,所以这个方法就很好理解了,只是对Element数组进行遍历,一旦找到类名与name相同的类时,就直接返回这个class,找不到则返回null。
为什么是调用DexFile的loadClassBinaryName()方法来加载class?这是因为一个Element对象对应一个dex文件,而一个dex文件则包含多个class。也就是说Element数组中存放的是一个个的dex文件,而不是class文件!!!这可以从Element这个类的源码和dex文件的内部结构看出。
2.热修复的实现方法
加载class会使用BaseDexClassLoader,在加载时,会遍历文件下的element,并从element中获取dex文件方案 ,class文件在dex里面 , 找到dex的方法是遍历数组 , 那么热修复的原理, 就是将改好bug的dex文件放进集合的头部, 这样遍历时会首先遍历修复好的dex并找到修复好的类 . 这样 , 我们就能在没有发布新版本的情况下 , 修改现有的bug。虽然我们无法改变现有的dex文件,但是遍历的顺序是从前往后的,在旧dex中的目标class是没有机会上场的。
3.手撸一个热修复Demo
在了解了大致的热修复过程之后,我们要准备好以下几个东西:
带有bug的apk,并且可以获取到dex文件来修复已修复bug的dex文件因为修复工作是需要隐秘的进行的 , 毕竟有bug也不是什么光彩的事儿 , 所以我吧dex的插入操作放在Splash界面中. 在Splash时先检测有没有dex文件, 如果有则进行插入 , 否则直接进入MainActivity.1->写一个有bug的程序哇, 是不是第一次见到这么爽的需求~首先在MainActivty中写一个bug出来:
public class BugTest { public void getBug(Context context) { //模拟一个bug int i = 10; int a = 0; Toast.makeText(context, "Hello,Minuit:" + i / a, Toast.LENGTH_SHORT).show(); }}public class MainActivity extends AppCompatActivity { Button btnFix; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); new BugTest().getBug(MainActivity.this); } private void init() { btnFix = findViewById(R.id.btn_fix); }}
运行这段代码必然会报错的 , 但是我们要首先吧这段代码装到手机上 , 方便之后的修复.
接下来编写SplashActivity以及工具类 . 大家可以根据具体逻辑修改
/***@author Minuit*@time 2018/6/25 0025 15:50*/public class FixDexUtil { 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) { // dex合并之前的dex doDexInject(context, loadedDex); } /** *@author Minuit *@time 2018/6/25 0025 15:51 *@desc 验证是否需要热修复 */ public static boolean isGoingToFix(@NonNull Context context) { boolean canFix = false; File externalStorageDirectory = Environment.getExternalStorageDirectory(); // 遍历所有的修复dex , 因为可能是多个dex修复包 File fileDir = externalStorageDirectory != null ? externalStorageDirectory : 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文件, 需要修复 canFix = true; } } return canFix; } 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的Loader PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader(); for (File dex : loadedDex) { // 2.加载指定的修复的dex文件的Loader DexClassLoader dexLoader = new DexClassLoader( dex.getAbsolutePath(),// 修复好的dex(补丁)所在目录 fopt.getAbsolutePath(),// 存放dex的解压目录(用于jar、zip、apk格式的补丁) null,// 加载dex时需要的库 pathLoader// 父类加载器 ); // 3.开始合并 // 合并的目标是Element[],重新赋值它的值即可 /** * BaseDexClassLoader中有 变量: DexPathList pathList * DexPathList中有 变量 Element[] dexElements * 依次反射即可 */ //3.1 准备好pathList的引用 Object dexPathList = getPathList(dexLoader); Object pathPathList = getPathList(pathLoader); //3.2 从pathList中反射出element集合 Object leftDexElements = getDexElements(dexPathList); Object rightDexElements = getDexElements(pathPathList); //3.3 合并两个dex数组 Object dexElements = combineArray(leftDexElements, rightDexElements); // 重写给PathList里面的Element[] dexElements;赋值 Object pathList = getPathList(pathLoader);// 一定要重新获取,不要用pathPathList,会报错 setField(pathList, pathList.getClass(), "dexElements", dexElements); } Toast.makeText(appContext, "修复完成", Toast.LENGTH_SHORT).show(); } 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<?> clazz = arrayLhs.getClass().getComponentType(); int i = Array.getLength(arrayLhs);// 得到左数组长度(补丁数组) int j = Array.getLength(arrayRhs);// 得到原dex数组长度 int k = i + j;// 得到总数组长度(补丁数组+原dex数组) Object result = Array.newInstance(clazz, k);// 创建一个类型为clazz,长度为k的新数组 System.arraycopy(arrayLhs, 0, result, 0, i); System.arraycopy(arrayRhs, 0, result, i, j); return result; }}
接下来 , 我们在Splash中进行检测以及修复工作
if (FixDexUtil.isGoingToFix(activity)) { FixDexUtil.loadFixedDex(activity, Environment.getExternalStorageDirectory()); } new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(2000); startActivity(new Intent(activity,MainActivity.class)); finish(); } catch (InterruptedException e) { e.printStackTrace(); } } }).start();
接下来 , 在As中一定一定要把instance run取消勾选,因为instance run用到的原理也是热修复的原理,也就是在重新运行app时不会完整的安装,只会安装你修改过的代码。
编译运行:
恩 , 接下来我们要修复bug,并且将修复好的包放进sd卡里面,这样在Splash开始时就会自动遍历到dex。
2->编写修复好的dex定位一下bug是出现在BugTest 中 , 所以我们首先修复bug
public void getBug(Context context) { int i = 10; int a = 1; Toast.makeText(context, "Hello,Minuit:" + i / a, Toast.LENGTH_SHORT).show(); }
然后将class文件打包成dex文件首先点击Build->Rebuild Project 来重新构建, 构建完成之后, 可以在app / build / interintermediate / debug / 包名/ 找到你刚刚修改的class文件 , 将他拷贝出来
注意 , 拷贝出来要连同包名一起, 像这样
因为在dex中的class文件是包名.类名的形式 , 所以我们在做dex文件时, 也要讲相对应的包名加上 . 这里反编译一个demo作为例子:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后
那我们该怎么做才能做到年薪60万+呢,对于程序员来说,只有不断学习,不断提升自己的实力。我之前有篇文章提到过,感兴趣的可以看看,到底要学习哪些知识才能达到年薪60万+。
通过职友集数据可以查看,以北京 Android 相关岗位为例,其中 【20k-30k】 薪酬的 Android 工程师,占到了整体从业者的 30.8%!
北京 Android 工程师「工资收入水平 」
今天重点内容是怎么去学,怎么提高自己的技术。
1.合理安排时间
2.找对好的系统的学习资料
3.有老师带,可以随时解决问题
4.有明确的学习路线
当然图中有什么需要补充的或者是需要改善的,可以在评论区写下来,一起交流学习。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
mg-2B6Q82fC-1713619592525)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!