Android之热修复实战(1),安卓开发自学技巧

对每个函数都在编译打包阶段自动的插入了一段代码。类似于代理,将方法执行的代码重定向到其他方法中。

Tinker

Tinker通过计算对比指定的Base Apk中的dex与修改后的Apk中的dex的区别,补丁包中的内容即为两者差分的描述。

运行时将Base Apk中的dex与补丁包进行合成,重启后加载全新的合成后的dex文件。

Qzone

2.ClassLoader


双亲委托机制

某个类加载器在加载类时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务或者没有父类加载器时,才自己去加载。

1、避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次。

2、安全性考虑,防止核心API库被随意篡改。

在线源码阅读:www.androidos.net.cn/或http://and…

类查找流程

类加载实现热修复

类是怎么被加载的? 怎么使用补丁包中的类? 已经被加载过的类还能够替换修复吗? 怎样保证补丁包中正确class 的dex先加载?

加载修复后的类

EnjoyFix.installPatch(this, new File(“/sdcard/patch.jar”));

EnjoyFix.class

public class EnjoyFix {

private static final String TAG = “EnjoyFix”;

private static File initHack(Context context) {

File hackFile = new File(context.getExternalFilesDir(“”), “hack.dex”);

FileOutputStream fos = null;

InputStream is = null;

try {

fos = new FileOutputStream(hackFile);

is = context.getAssets().open(“hack.dex”);

int len;

byte[] buffer = new byte[2048];

while ((len = is.read(buffer)) != -1) {

fos.write(buffer, 0, len);

}

} catch (IOException e) {

e.printStackTrace();

} finally {

if (fos != null) {

try {

fos.close();

} catch (IOException e) {

e.printStackTrace();

}

}

if (is != null) {

try {

is.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

return hackFile;

}

/**

  • 1、获取程序的PathClassLoader对象

  • 2、反射获得PathClassLoader父类BaseDexClassLoader的pathList对象

  • 3、反射获取pathList的dexElements对象 (oldElement)

  • 4、把补丁包变成Element数组:patchElement(反射执行makePathElements)

  • 5、合并patchElement+oldElement = newElement (Array.newInstance)

  • 6、反射把oldElement赋值成newElement

  • @param application

  • @param patch

*/

public static void installPatch(Application application, File patch) {

File hackDex = initHack(application);

List patchs = new ArrayList<>();

patchs.add(hackDex);

if (patch.exists()) {

patchs.add(patch);

}

//1、获取程序的PathClassLoader对象

ClassLoader classLoader = application.getClassLoader();

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {

try {

ClassLoaderInjector.inject(application, classLoader, patchs);

} catch (Throwable throwable) {

}

return;

}

//2、反射获得PathClassLoader父类BaseDexClassLoader的pathList对象

try {

Field pathListField = ShareReflectUtil.findField(classLoader, “pathList”);

Object pathList = pathListField.get(classLoader);

//3、反射获取pathList的dexElements对象 (oldElement)

Field dexElementsField = ShareReflectUtil.findField(pathList, “dexElements”);

Object[] oldElements = (Object[]) dexElementsField.get(pathList);

//4、把补丁包变成Element数组:patchElement(反射执行makePathElements)

Object[] patchElements = null;

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

Method makePathElements = ShareReflectUtil.findMethod(pathList, “makePathElements”,

List.class, File.class,

List.class);

ArrayList ioExceptions = new ArrayList<>();

patchElements = (Object[])

makePathElements.invoke(pathList, patchs, application.getCacheDir(), ioExceptions);

} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {

Method makePathElements = ShareReflectUtil.findMethod(pathList, “makeDexElements”,

ArrayList.class, File.class, ArrayList.class);

ArrayList ioExceptions = new ArrayList<>();

patchElements = (Object[])

makePathElements.invoke(pathList, patchs, application.getCacheDir(), ioExceptions);

}

//5、合并patchElement+oldElement = newElement (Array.newInstance)

//创建一个新数组,大小 oldElements+patchElements

// int[].class.getComponentType() ==int.class

Object[] newElements = (Object[]) Array.newInstance(oldElements.getClass().getComponentType(),

oldElements.length + patchElements.length);

System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);

System.arraycopy(oldElements, 0, newElements, patchElements.length, oldElements.length);

//6、反射把oldElement赋值成newElement

dexElementsField.set(pathList, newElements);

} catch (Exception e) {

e.printStackTrace();

}

}

}

3.插桩式热修复运行期修复落地


热修复流程

1.获取到当前应用的PathClassloader; 2.反射获取到DexPathList属性对象pathList; 3.反射修改pathList的dexElements

  • 把补丁包patch.dex转化为Element[] (patch)

  • 获得pathList的dexElements属性(old)

  • patch+dexElements合并,并反射赋值给pathList的dexElements

Element数组

补丁包其实就是一个dex或者包含dex的jar包。怎么把dex变成Element?

Android N混合编译

ART 是在 Android KitKat(Android 4.0)引入并在 Lollipop(Android 5.0)中设为默认运行环境,可以看作Dalvik2.0。ART模式在Android N(7.0)之前安装APK时会采用AOT(Ahead of time:提前编译、静态编译)预编译为机器码。而在Android N使用混合模式的运行时。应用在安装时不做编译,而是运行时解释字节码,同时在JIT编译了一 些代码后将这些代码信息记录至Profile文件,等到设备空闲的时候使用AOT(All-Of-the-Time compilation:全 时段编译)编译生成称为app_image的base.art(类对象映像)文件,这个art文件会在apk启动时自动加载(相当 于缓存)。根据类加载原理,类被加载了无法被替换,即无法修复。

解决方案

运行时替换PathClassLoader方案 App image中的class是插入到PathClassloader中的ClassTable中。假设我们完全废弃掉PathClassloader,而 采用一个新建Classloader来加载后续的所有类,即可达到将cache无用化的效果。

CLASS_ISPREVERIFIED异常

如果MainActivity类中只引用了:Utils类。当打包dex时, MainActivity与Utils都在classes.dex中,则加载 时MainActivity类被标记为 CLASS_ISPREVERIFIED。

如果使用补丁包中的Utils类取代出现bug的Utils,则会导致MainActivity与其引用的Utils不在同一个Dex, 但MainActivity已经被打上标记,此时出现冲突。导致校验失败!

解决方案

阻止标记:防止类被打上CLASS_ISPREVERIFIED标志。

AntilazyLoad类会被打包成单独的hack.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的 AntilazyLoad类,防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作。

字节码插桩:在Java字节码Class中某些位置插入或修改一些代码。

4.字节码插桩


在Java字节码中某些位置插入或修改一些代码。

Android打包流程

  • AAPT :打包资源文件,并生成R.java和resources.arsc文件。

  • AIDI : 读取.aidl文件生成对应Java文件

  • JAVAC : 将java文件编译成.class文件;

  • DX : 多个.class文件转换打包为.dex文件

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

结语

看到这篇文章的人不知道有多少是和我一样的Android程序员。

35岁,这是我们这个行业普遍的失业高发阶段,这种情况下如果还不提升自己的技能,进阶发展,我想,很可能就是本行业的职业生涯的终点了。

我们要有危机意识,切莫等到一切都成定局时才开始追悔莫及。只要有规划的,有系统地学习,进阶提升自己并不难,给自己多充一点电,你才能走的更远。

千里之行始于足下。这是上小学时,那种一元钱一个的日记本上每一页下面都印刷有的一句话,当时只觉得这句话很短,后来渐渐长大才慢慢明白这句话的真正的含义。

有了学习的想法就赶快行动起来吧,不要被其他的事情牵绊住了前行的脚步。不要等到裁员时才开始担忧,不要等到面试前一晚才开始紧张,不要等到35岁甚至更晚才开始想起来要学习要进阶。

给大家一份系统的Android学习进阶资料,希望这份资料可以给大家提供帮助。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

阶段,这种情况下如果还不提升自己的技能,进阶发展,我想,很可能就是本行业的职业生涯的终点了。

我们要有危机意识,切莫等到一切都成定局时才开始追悔莫及。只要有规划的,有系统地学习,进阶提升自己并不难,给自己多充一点电,你才能走的更远。

千里之行始于足下。这是上小学时,那种一元钱一个的日记本上每一页下面都印刷有的一句话,当时只觉得这句话很短,后来渐渐长大才慢慢明白这句话的真正的含义。

有了学习的想法就赶快行动起来吧,不要被其他的事情牵绊住了前行的脚步。不要等到裁员时才开始担忧,不要等到面试前一晚才开始紧张,不要等到35岁甚至更晚才开始想起来要学习要进阶。

给大家一份系统的Android学习进阶资料,希望这份资料可以给大家提供帮助。
[外链图片转存中…(img-G8Ji5qxX-1712378187478)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 16
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值