Android热补丁动态修复技术(二):实战!CLASS_ISPREVERIFIED问题!

本文主要是介绍Android热补丁动态修复技术(二):实战!CLASS_ISPREVERIFIED问题!,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、前言

上一篇博客中,我们通过介绍dex分包原理引出了Android的热补丁技术,而现在我们将解决两个问题。

  1. 怎么将修复后的Bug类打包成dex
  2. 怎么将外部的dex插入到ClassLoader中

二、建立测试Demo

2.1 目录结构

这里写图片描述

2.2 源码

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:paddingBottom="@dimen/activity_vertical_margin"android:paddingLeft="@dimen/activity_horizontal_margin"android:paddingRight="@dimen/activity_horizontal_margin"android:paddingTop="@dimen/activity_vertical_margin"tools:context=".MainActivity"><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:onClick="click"android:text="小喵叫一声"/>
</RelativeLayout>

MainActivity.class

package com.aitsuki.bugfix;import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;
import com.aitsuki.bugfix.animal.Cat;public class MainActivity extends AppCompatActivity {private Cat mCat;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mCat = new Cat();}public void click(View view) {Toast.makeText(this, mCat.say(),Toast.LENGTH_SHORT).show();}
}

Cat.class

package com.aitsuki.bugfix.animal;/*** Created by AItsuki on 2016/3/14.*/
public class Cat {public String say() {return "汪汪汪!";}
}

2.3 运行结果

这里写图片描述

假设这是我们公司的开发项目,刚刚上线就发现了严重bug,猫会狗叫。
想修复bug,让用户再立刻更新一次显然很不友好,此时热补丁修复技术就有用了。

三、制作补丁

在加载dex的代码之前,我们先来制作补丁。

  1. 首先我们将Cat类修复,汪汪汪改成喵喵喵,然后重新编译项目。(Rebuild一下就行了)
  2. 去保存项目的地方,将Cat.class文件拷贝出来,在这里
    这里写图片描述
  3. 新建文件夹,要和该Cat.class文件的包名一致,然后将Cat.class复制到这里,如图
    这里写图片描述
  4. 命令行进入到图中的test目录,运行一下命令,打包补丁。如图:
    这里写图片描述
    然后test目录是这样的
    这里写图片描述
    patch_dex.jar就是我们打包好的补丁了,我们将它放到sdCard中,待会从这里加载补丁。

关于什么用这么复杂的方法打包补丁的说明:
你也可以直接将java文件拷出来,通过javac -d带包编译再转成jar。
但我这么麻烦是有原因的,因为用这种方法你可能会遇到ParseException,原因是jar包版本和dx工具版本不一致。
而从项目中直接将编译好的class直接转成jar就没问题,因为java会向下兼容,打出来的jar包和class版本是一致的。
总而言之,dx版本要和class编译版本对应。

##四、加载补丁

4.1 思路

通过上一篇博文,我们知道dex保存在这个位置
BaseDexClassLoader–>pathList–>dexElements

  1. apk的classes.dex可以从应用本身的DexClassLoader中获取。
  2. path_dex的dex需要new一个DexClassLoader加载后再获取。
  3. 分别通过反射取出dex文件,重新合并成一个数组,然后赋值给盈通本身的ClassLoader的dexElements

4.2 代码实现

加载外部dex,我们可以在Application中操作。
首先新建一个HotPatchApplication,然后在清单文件中配置,顺便加上读取sdcard的权限,因为补丁就保存在那里。

HotPatchApplication代码如下:

package com.aitsuki.hotpatchdemo;import android.app.Application;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.DexClassLoader;/*** Created by hp on 2016/4/6.*/
public class HotPatchApplication extends Application {@Overridepublic void onCreate() {super.onCreate();// 获取补丁,如果存在就执行注入操作String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch_dex.jar");File file = new File(dexPath);if (file.exists()) {inject(dexPath);} else {Log.e("BugFixApplication", dexPath + "不存在");}}/*** 要注入的dex的路径** @param path*/private void inject(String path) {try {// 获取classes的dexElementsClass<?> cl = Class.forName("dalvik.system.BaseDexClassLoader");Object pathList = getField(cl, "pathList", getClassLoader());Object baseElements = getField(pathList.getClass(), "dexElements", pathList);// 获取patch_dex的dexElements(需要先加载dex)String dexopt = getDir("dexopt", 0).getAbsolutePath();DexClassLoader dexClassLoader = new DexClassLoader(path, dexopt, dexopt, getClassLoader());Object obj = getField(cl, "pathList", dexClassLoader);Object dexElements = getField(obj.getClass(), "dexElements", obj);// 合并两个ElementsObject combineElements = combineArray(dexElements, baseElements);// 将合并后的Element数组重新赋值给app的classLoadersetField(pathList.getClass(), "dexElements", pathList, combineElements);//======== 以下是测试是否成功注入 =================Object object = getField(pathList.getClass(), "dexElements", pathList);int length = Array.getLength(object);Log.e("BugFixApplication", "length = " + length);} catch (ClassNotFoundException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (NoSuchFieldException e) {e.printStackTrace();}}/*** 通过反射获取对象的属性值*/private Object getField(Class<?> cl, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException {Field field = cl.getDeclaredField(fieldName);field.setAccessible(true);return field.get(object);}/*** 通过反射设置对象的属性值*/private void setField(Class<?> cl, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException {Field field = cl.getDeclaredField(fieldName);field.setAccessible(true);field.set(object, value);}/*** 通过反射合并两个数组*/private Object combineArray(Object firstArr, Object secondArr) {int firstLength = Array.getLength(firstArr);int secondLength = Array.getLength(secondArr);int length = firstLength + secondLength;Class<?> componentType = firstArr.getClass().getComponentType();Object newArr = Array.newInstance(componentType, length);for (int i = 0; i < length; i++) {if (i < firstLength) {Array.set(newArr, i, Array.get(firstArr, i));} else {Array.set(newArr, i, Array.get(secondArr, i - firstLength));}}return newArr;}}

五、CLASS_ISPREVERIFIED

运行一下Demo,报以下错误。(AndroidStudio 2.0可能不会报错,需要打包的时候才会出现错误,这是Instant run导致的)
这里写图片描述
dexElements的length = 2,看来我们的patch_dex已经成功添加进去了。
但是从黄色框框和黄色框上面那一段log提示中可以看出,MainActivity引用了Cat,但是发现他们在不同的Dex中。

看到这里可能就会问:
为什么之前那么多项目都采用分包方案,但是却不会出现这个错误呢?
我在这里总结了一个过程,想知道详细分析过程的请看QQ空间开发团队的原文。

  1. 在apk安装的时候,虚拟机会将dex优化成odex后才拿去执行。在这个过程中会对所有class一个校验。
  2. 校验方式:假设A该类在它的static方法,private方法,构造函数,override方法中直接引用到B类。如果A类和B类在同一个dex中,那么A类就会被打上CLASS_ISPREVERIFIED标记
  3. 被打上这个标记的类不能引用其他dex中的类,否则就会报图中的错误
  4. 在我们的Demo中,MainActivity和Cat本身是在同一个dex中的,所以MainActivity被打上了CLASS_ISPREVERIFIED。而我们修复bug的时候却引用了另外一个dex的Cat.class,所以这里就报错了
  5. 而普通分包方案则不会出现这个错误,因为引用和被引用的两个类一开始就不在同一个dex中,所以校验的时候并不会被打上CLASS_ISPREVERIFIED
  6. 补充一下第二条:A类如果还引用了一个C类,而C类在其他dex中,那么A类并不会被打上标记。换句话说,只要在static方法,构造方法,private方法,override方法中直接引用了其他dex中的类,那么这个类就不会被打上CLASS_ISPREVERIFIED标记。

5.1 解决方案

根据上面的第六条,我们只要让所有类都引用其他dex中的某个类就可以了。

下面是QQ控件给出的解决方案
这里写图片描述

  1. 在所有类的构造函数中插入这行代码 System.out.println(AntilazyLoad.class);
    这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作。
  2. hack.dex在应用启动的时候就要先加载出来,不然AntilazyLoad类会被标记为不存在,即使后面再加载hack.dex,AntilazyLoad类还是会提示不存在。该类只要一次找不到,那么就会永远被标上找不到的标记了。
  3. 我们一般在Application中执行dex的注入操作,所以在Application的构造中不能加上System.out.println(AntilazyLoad.class);这行代码,因为此时hack.dex还没有加载进来,AntilazyLoad并不存在。
  4. 之所以选择构造函数是因为他不增加方法数,一个类即使没有显式的构造函数,也会有一个隐式的默认构造函数。

5.2 插入代码的难点

  1. 首先在源码中手动插入不太可行,hack.dex此时并没有加载进来,AntilazyLoad.class并不存在,编译不通过。
  2. 所以我们需要在源码编译成字节码之后,在字节码中进行插入操作。对字节码进行操作的框架有很多,但是比较常用的则是ASM和javaassist
  3. 但AndroidStudio是使用Gradle构建项目,编译-打包都是自动化的,我们怎么操作呢。敬请期待下一篇博客

六、写在后面

其实整个热补丁技术最难的地方不是原理,不是注入dex,而是字节码的注入。
这需要我们队Gradle构建脚本,Groovy语言有一定的了解。其中的知识量实在是太过庞大,这里推荐几篇博文预习一下。
Gradle学习系列之一——Gradle快速入门
深入理解Android之Gradle——by 阿拉神农

ps:有些朋友可能会发现我的一些图片存在问题…… 比如运行结果那张图,标题是Bugfix。
命令行那张图,进的是blog目录……
因为研究这个热补丁技术的周期比较长,而且是一边写博客,所以有些图片弄错了……

这篇关于Android热补丁动态修复技术(二):实战!CLASS_ISPREVERIFIED问题!的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/984976

相关文章

linux生产者,消费者问题

pthread_cond_wait() :用于阻塞当前线程,等待别的线程使用pthread_cond_signal()或pthread_cond_broadcast来唤醒它。 pthread_cond_wait() 必须与pthread_mutex 配套使用。pthread_cond_wait()函数一进入wait状态就会自动release mutex。当其他线程通过pthread

问题:第一次世界大战的起止时间是 #其他#学习方法#微信

问题:第一次世界大战的起止时间是 A.1913 ~1918 年 B.1913 ~1918 年 C.1914 ~1918 年 D.1914 ~1919 年 参考答案如图所示

乐鑫 Matter 技术体验日|快速落地 Matter 产品,引领智能家居生态新发展

随着 Matter 协议的推广和普及,智能家居行业正迎来新的发展机遇,众多厂商纷纷投身于 Matter 产品的研发与验证。然而,开发者普遍面临技术门槛高、认证流程繁琐、生产管理复杂等诸多挑战。  乐鑫信息科技 (688018.SH) 凭借深厚的研发实力与行业洞察力,推出了全面的 Matter 解决方案,包含基于乐鑫 SoC 的 Matter 硬件平台、基于开源 ESP-Matter SDK 的一

一份LLM资源清单围观技术大佬的日常;手把手教你在美国搭建「百万卡」AI数据中心;为啥大模型做不好简单的数学计算? | ShowMeAI日报

👀日报&周刊合集 | 🎡ShowMeAI官网 | 🧡 点赞关注评论拜托啦! 1. 为啥大模型做不好简单的数学计算?从大模型高考数学成绩不及格说起 司南评测体系 OpenCompass 选取 7 个大模型 (6 个开源模型+ GPT-4o),组织参与了 2024 年高考「新课标I卷」的语文、数学、英语考试,然后由经验丰富的判卷老师评判得分。 结果如上图所

2024.6.24 IDEA中文乱码问题(服务器 控制台 TOMcat)实测已解决

1.问题产生原因: 1.文件编码不一致:如果文件的编码方式与IDEA设置的编码方式不一致,就会产生乱码。确保文件和IDEA使用相同的编码,通常是UTF-8。2.IDEA设置问题:检查IDEA的全局编码设置和项目编码设置是否正确。3.终端或控制台编码问题:如果你在终端或控制台看到乱码,可能是终端的编码设置问题。确保终端使用的是支持你的文件的编码方式。 2.解决方案: 1.File -> S

持久层 技术选型如何决策?JPA,Hibernate,ibatis(mybatis)

转自:http://t.51jdy.cn/thread-259-1-1.html 持久层 是一个项目 后台 最重要的部分。他直接 决定了 数据读写的性能,业务编写的复杂度,数据结构(对象结构)等问题。 因此 架构师在考虑 使用那个持久层框架的时候 要考虑清楚。 选择的 标准: 1,项目的场景。 2,团队的技能掌握情况。 3,开发周期(开发效率)。 传统的 业务系统,通常业

vcpkg安装opencv中的特殊问题记录(无法找到opencv_corexd.dll)

我是按照网上的vcpkg安装opencv方法进行的(比如这篇:从0开始在visual studio上安装opencv(超详细,针对小白)),但是中间出现了一些别人没有遇到的问题,虽然原因没有找到,但是本人给出一些暂时的解决办法: 问题1: 我在安装库命令行使用的是 .\vcpkg.exe install opencv 我的电脑是x64,vcpkg在这条命令后默认下载的也是opencv2:x6

问题-windows-VPN不正确关闭导致网页打不开

为什么会发生这类事情呢? 主要原因是关机之前vpn没有关掉导致的。 至于为什么没关掉vpn会导致网页打不开,我猜测是因为vpn建立的链接没被更改。 正确关掉vpn的时候,会把ip链接断掉,如果你不正确关掉,ip链接没有断掉,此时你vpn又是没启动的,没有域名解析,所以就打不开网站。 你可以在打不开网页的时候,把vpn打开,你会发现网络又可以登录了。 方法一 注意:方法一虽然方便,但是可能会有

亮相WOT全球技术创新大会,揭秘火山引擎边缘容器技术在泛CDN场景的应用与实践

2024年6月21日-22日,51CTO“WOT全球技术创新大会2024”在北京举办。火山引擎边缘计算架构师李志明受邀参与,以“边缘容器技术在泛CDN场景的应用和实践”为主题,与多位行业资深专家,共同探讨泛CDN行业技术架构以及云原生与边缘计算的发展和展望。 火山引擎边缘计算架构师李志明表示:为更好地解决传统泛CDN类业务运行中的问题,火山引擎边缘容器团队参考行业做法,结合实践经验,打造火山

React+TS前台项目实战(十七)-- 全局常用组件Dropdown封装

文章目录 前言Dropdown组件1. 功能分析2. 代码+详细注释3. 使用方式4. 效果展示 总结 前言 今天这篇主要讲全局Dropdown组件封装,可根据UI设计师要求自定义修改。 Dropdown组件 1. 功能分析 (1)通过position属性,可以控制下拉选项的位置 (2)通过传入width属性, 可以自定义下拉选项的宽度 (3)通过传入classN