从java层实现Tinker热修复
作者:互联网
Tinker热修复
代码中的注释别忘记看!!!!!
学习内容
一、Android中的 类加载器
首先我们需要了解类加载器,我们要明白我们所有类的加载都是通过getClassLoader().loadClass();
,这是我们开始热修复的重要前提。先提出一个问题,大家知道ClassLoader classLoader = getClassLoader();
这个classLoader是哪个对象嘛?肯定不是ClassLoader,不然也没有必要说,其实是PathClassLoader,其实在Android中类加载器有很多个,如下
类名 | 作用 |
---|---|
BootClassLoader | Android系统启动的时候会使用这个来预加载常用类 |
PathClassLoader | 其实这个就是加载已经安装好的APK,也可以加载lib |
DexClassLoader | 用来加载当前应用以外的(第三方的dex或者lib) |
多讲一句:PathClassLoader和DexClassLoader都继承BaseDexClassLoader(继承ClassLoader)
下面我们通过阅读源码的方式来换个角度看 在线阅读源码链接
既然getClassLoader返回的是PathClassLoader,那我们就来看看到底是个啥东西。
- PathClassLoader.class
1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package dalvik.system;
18
19/**
20 * Provides a simple {@link ClassLoader} implementation that operates on a list
21 * of files and directories in the local file system, but does not attempt to
22 * load classes from the network. Android uses this class for its system class
23 * loader and for its application class loader(s).
24 */
25 public class PathClassLoader extends BaseDexClassLoader {
26 /**
27 * Creates a {@code PathClassLoader} that operates on a given list of files
28 * and directories. This method is equivalent to calling
29 * {@link #PathClassLoader(String, String, ClassLoader)} with a
30 * {@code null} value for the second argument (see description there).
31 *
32 * @param dexPath the list of jar/apk files containing classes and
33 * resources, delimited by {@code File.pathSeparator}, which
34 * defaults to {@code ":"} on Android
35 * @param parent the parent class loader
36 */
37 public PathClassLoader(String dexPath, ClassLoader parent) {
38 super(dexPath, null, null, parent);
39 }
40
41 /**
42 * Creates a {@code PathClassLoader} that operates on two given
43 * lists of files and directories. The entries of the first list
44 * should be one of the following:
45 *
46 * <ul>
47 * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
48 * well as arbitrary resources.
49 * <li>Raw ".dex" files (not inside a zip file).
50 * </ul>
51 *
52 * The entries of the second list should be directories containing
53 * native library files.
54 *
55 * @param dexPath the list of jar/apk files containing classes and
56 * resources, delimited by {@code File.pathSeparator}, which
57 * defaults to {@code ":"} on Android
58 * @param librarySearchPath the list of directories containing native
59 * libraries, delimited by {@code File.pathSeparator}; may be
60 * {@code null}
61 * @param parent the parent class loader
62 */
63 public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
64 super(dexPath, null, librarySearchPath, parent);
65 }
66}
我们发现这个类里面居然只有构造函数,我们很失望,但我们发现这个类是继承BaseDexClassLoader,所以你懂的。
- BaseDexClassLoader.class
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;
}
........
}
这里我们先关注一下这个方法,我们会用到,这里重写了ClassLoader 的findclass 方法。
- ClassLoader.class(记得看程序中注释)
public abstract class ClassLoader {
.....
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
//这里的意思是,检查当前的类是否已经加载过。
//为什么要这样呢,因为我们的apk里其实有很多.dex后缀文件,每个dex文件里面存放了我们写
//好的class文件,所以每当我们需要创建相应的class,就去遍历这些dex文件,然后进行实例
//化,但是我们考虑这样一个问题,难道每次创建都需要去dex查询创建,这样就很耗性能,所以
//把已经加载过的类会缓存起来,这样放边下次直接拿出类就可以使用。所以这个findLoadedClass
//就是在曾经加载过的类中的缓存里查找是否加载过,加载过就直接拿出来。
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;
}
.......
}
Class class = getClassLoader().loadClass(“包名+类名”);
大家看这个代码,所有类的加载都是通过这样的方法加载的,大家可以看上面的代码,这是博主截取的部分代码,这就是加载类的核心源码了,大家可以看到其中有一个findClass()。
上面说过getClassLoader返回的是PathClassLoader(继承BaseDexClassLoader),本质上findClass就是调用了BaseDexClassLoader.findclass()
所以重点来了,我们现在只要关注在BaseDexClassLoader的findclass实现方法就行,这里代码就再贴一下吧。
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(),所以再深入一步。
- DexPathList .class
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).
*/
//怎么理解这个变量呢?
//上面我们说过,我们的apk里有多个dex文件,这些文件里有许多的class,
//我们每次查找class都需要到这些dex文件里面查找。
//所以这个数组其实就是我们每个dex文件,googole把每个dex文件
//分装成了Element,所以说每个Element就代表了一个dex文件。
private Element[] dexElements;
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
//这里在循环查找dex文件里的每个element是否为目标class
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
.....
}
恭喜你,看到这的话,你已经基本了解了Android类加载的过程
你还不能放松,现在才到一半,你会疑问为什么热修复跟这个有什么关系,其实不然,因为我们知道我们加载类是去dex文件里去查找相应的class文件然后实例化,但假如现在有个helloActivity.class里面存在bug,我们应该怎么办,当然第一步就是解决bug,然后把这个类打包成dex格式,当然也可以是library,apk都行,这里我们就先按照dex格式说明,然后让os先去我们这个刚修改过的dex文件里查找helloActivity.class,这样就把bug解决掉了,但是有个问题让os先去我们这个刚修改过的dex文件里查找?这就要讲到HOOK技术了,我们需要通过Hook的技术手段,使得我们修改好的dex文件也能被os所识别到。
接下来,重头戏来了。
二、进行代码的实现
- 第一步 :
我们需要首先创建一个文件夹(fix),然后切换成project模式,在app/intermediates/javac/debug/这个目录下面找到你修改好生成的class文件(注意这里要带上包名的文件夹,比如全路径为
fix/com/example/oicq/helloActivity.class),然后进行打包,参考下面的博客
- 第二步:就是需要让用户在后台下载这个修复包
这一步你可通过服务器也好,手动上传的方式也行,只要能上传到你的手机上sd卡根目录就行。
当然你也可以放在别的地方,我们这里放在sd卡下面,只是做一个缓存,我们下面的程序,会把上传的dex修复包移动到我们app私密的路径里,这样就防止用户删除我们修复包。 - 第三步:进行hook操作,实现热修复。(需要在Application里调用startFix())
public class FixManager {
//存储所有修复包路径
private static HashSet<File> loadedDex = new HashSet<>();
static {
loadedDex.clear();
}
public static void startFix(Context context){
//将这个修复包先移动一个安全不易被用户删除的目录/data/data/包名/odex
File odex = context.getDir("odex",Context.MODE_PRIVATE);
String name = "fix.dex";
File file = new File(odex.getAbsolutePath(),name);
if(file.exists()) file.delete();
String filePath = file.getAbsolutePath();
FileInputStream is = null;
FileOutputStream os = null;
try{
//这里我手动把补丁包上传到sd卡路径根目录下(为了安全考虑)
is = new FileInputStream(new File(Environment.getExternalStorageDirectory(),name));
os = new FileOutputStream(filePath);
int len = 0;
byte[] buffer = new byte[1024];
while((len = is.read(buffer))!=-1){
os.write(buffer,0,len);
}
//开始修复
FixDex(context);
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(is!= null) is.close();
if(os!= null) os.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
private static void FixDex(Context context){
if(null == context) return;
File odex = context.getDir("odex",Context.MODE_PRIVATE);
File[] fileList = odex.listFiles();
for(File file:fileList){
if(!file.getName().endsWith(".dex")) continue;
loadedDex.add(file);
}
//创建外部dex文件的缓存目录
String temFileDir = odex.getAbsolutePath() + File.separator+"tempFile";
File temFile = new File(temFileDir);
if(!temFile.exists()) temFile.mkdirs();
for(File file:loadedDex) {
try {
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
//反射开始(先得到BaseDexClassLoader)
Class<?> superClass = pathClassLoader.getClass().getSuperclass();
//得到成员变量 pathList
Field pathListField = superClass.getDeclaredField("pathList");
//设置可以访问
pathListField.setAccessible(true);
//得到成员变量pathList的值(得到当前应用的值)
Object pathListValue = pathListField.get(pathClassLoader);
//获取到dexElements的成员变量
Field dexElementsField = pathListValue.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object dexElementsValue = dexElementsField.get(pathListValue);
//以上我们获取当前应用下所有的dex
//接下来我们要获取我们的补丁包
//首先要用classloader加载外部的dex文件,我们这里使用DexClassLoader,
//具体原因看上面的表格解释
//创建DexClassLoder
DexClassLoader dexClassLoader = new DexClassLoader(file.getAbsolutePath(),temFile.getAbsolutePath(),null,context.getClassLoader());
Object dexPathListValue = pathListField.get(dexClassLoader);
Object dexFixedElementsValue = dexElementsField.get(dexPathListValue);
//上面获取了外部补丁包的dex文件
//接下就是进行合并了
//这里我们需要把我们的外部dex文件放在当前应用dexlist的开始位置
//这是为了让系统在我们的已经修复好bug里面找,这样就不会找到之后
//存在问题的class了,还不看不懂,我等会儿画图就懂了
//接下来进行合并
int length1 = Array.getLength(dexElementsValue);
int length2 = Array.getLength(dexFixedElementsValue);
int newLength = length1 + length2;
//创建合并后的数组
Class<?> dataType = dexFixedElementsValue.getClass().getComponentType();
Object newDexElements = Array.newInstance(dataType,newLength);
for(int i=0;i<newLength;i++){
if(i<length2){
//先放我们修改的补丁包dex文件
Array.set(newDexElements,i,Array.get(dexFixedElementsValue,i));
}else{
//再放应用原来的dex文件
Array.set(newDexElements,i,Array.get(dexElementsValue,i));
}
}
//将合并后的数组赋值给当前应用的dexlist
dexElementsField.set(pathListValue,newDexElements);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
三、 总结
现在我们就完成了修复,现在考虑两个问题,
问: 上面这段代码需要每次应用都需bu要执行嘛,
答:是的,每次都要。
原因: 因为我们每次应用重启,我们ClassLoader的生命周期是跟Application生命周期一致的,所以我们每次都需要修复,所以上面这段代码我们需要在Appliation里的onCreate方法才行。
综上:我们这次实现的Tinker热修复是从java的角度通过反射实现,需要应用重启才可以进行修复,这也是我们这次的缺陷。但你想实现应用不重启实现热更新就需要从NDK的角度实现了。有时间会出一篇。
下记:
这里讲一下,我这里没有考虑应用的混淆和加固,请注意,如果有的话,需要进行代码混淆逻辑的解析的。
PS:纯属个人理解,希望指正!
标签:dex,java,修复,name,Tinker,File,null,class,加载 来源: https://blog.csdn.net/sunlifeall/article/details/111303399