• linkedu视频
  • 平面设计
  • 电脑入门
  • 操作系统
  • 办公应用
  • 电脑硬件
  • 动画设计
  • 3D设计
  • 网页设计
  • CAD设计
  • 影音处理
  • 数据库
  • 程序设计
  • 认证考试
  • 信息管理
  • 信息安全
菜单
linkedu.com
  • 网页制作
  • 数据库
  • 程序设计
  • 操作系统
  • CMS教程
  • 游戏攻略
  • 脚本语言
  • 平面设计
  • 软件教程
  • 网络安全
  • 电脑知识
  • 服务器
  • 视频教程
  • JavaScript
  • ASP.NET
  • PHP
  • 正则表达式
  • AJAX
  • JSP
  • ASP
  • Flex
  • XML
  • 编程技巧
  • Android
  • swift
  • C#教程
  • vb
  • vb.net
  • C语言
  • Java
  • Delphi
  • 易语言
  • vc/mfc
  • 嵌入式开发
  • 游戏开发
  • ios
  • 编程问答
  • 汇编语言
  • 微信小程序
  • 数据结构
  • OpenGL
  • 架构设计
  • qt
  • 微信公众号
您的位置:首页 > 程序设计 >Android > Android 热修复原理及Gradle插件源码解析(以Nuwa为例)

Android 热修复原理及Gradle插件源码解析(以Nuwa为例)

作者:网友 字体:[增加 减小] 来源:互联网 时间:2017-05-26

网友通过本文主要向大家介绍了android nuwa,android nuwa热修复,nuwa成长记,王者荣耀nuwa视频,nuwa成长记2等相关知识,希望对您有所帮助,也希望大家支持linkedu.com www.linkedu.com

Android 热修复原理及Gradle插件源码解析(以Nuwa为例)


现在,热修复的具体实现方案开源的也有很多,原理也大同小异,本篇文章以Nuwa为例,深入剖析。
Nuwa的github地址
https://github.com/jasonross/Nuwa
以及用于hotpatch生成的gradle插件地址
https://github.com/jasonross/NuwaGradle

而Nuwa的具体实现是根据QQ空间的热修复方案来实现的。从QQ空间终端开发团队的文章中可以总结出要进行热更新只需要满足下面两点就可以了:

动态加载补丁dex,并将补丁dex插入到dexElements最前面要实现热更新,需要热更新的类要防止被打上ISPREVERIFIED标记,关于这个标记,请阅读上面QQ空间团队的文章。

对于第一点,实现很简单,通过DexClassLoader对象,将补丁dex对象加载进来,再通过反射将补丁dex插入到dexElements最前面即可。具体可参考谷歌的Multidex的实现。

而对于第二点,关键就是如何防止类被打上ISPREVERIFIED这个标记。

简单来说,就是将所有类的构造函数中,引用另一个hack.dex中的类,这个类叫Hack.class,然后在加载补丁patch.dex前动态加载这个hack.dex,但是有一个类的构造函数中不能引用Hack.class,这个类就是Application类的子类,一旦这个类的构造函数中加入Hack.class这个类,那么程序运行时就会找不到Hack.class这个类,因为还没有被加载。也就是说,一个类直接引用到的类不在同一个dex中即可。这样,就能防止类被打上ISPREVERIFIED标记并能进行热更新。

我们先来看Nuwa的实现,再去看Nuwa的插件的实现。

使用Nuwa的时候需要在attachBaseContext方法中初始化

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    Nuwa.init(this);
}

这里写图片描述

Nuwa预先将Hack.class这个类(空实现)打成apk文件,放在asserts目录中,在init方法中,做的就是将asserts目录中的这个文件拷贝到文件目录下。

public static void init(Context context) {
        File dexDir = new File(context.getFilesDir(), DEX_DIR);
        dexDir.mkdir();

        String dexPath = null;
        try {
            dexPath = AssetUtils.copyAsset(context, HACK_DEX, dexDir);
        } catch (IOException e) {
            Log.e(TAG, "copy " + HACK_DEX + " failed");
            e.printStackTrace();
        }

        loadPatch(context, dexPath);
    }

首先创建文件目录将asserts目录下的hack.apk拷到该目录,然后调用loadPatch方法将该apk动态加载进来。loadPatch方法也是之后进行热修复的关键方法,你的所有补丁文件都是通过这个方法动态加载进来。

 public static void loadPatch(Context context, String dexPath) {

        if (context == null) {
            Log.e(TAG, "context is null");
            return;
        }
        if (!new File(dexPath).exists()) {
            Log.e(TAG, dexPath + " is null");
            return;
        }
        File dexOptDir = new File(context.getFilesDir(), DEX_OPT_DIR);
        dexOptDir.mkdir();
        try {
            DexUtils.injectDexAtFirst(dexPath, dexOptDir.getAbsolutePath());
        } catch (Exception e) {
            Log.e(TAG, "inject " + dexPath + " failed");
            e.printStackTrace();
        }
    }

loadPatch方法中主要是调用DexUtils.injectDexAtFirst()方法将dex插入到dexElements最前面。该方法如下。

public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
        Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
        Object newDexElements = getDexElements(getPathList(dexClassLoader));
        Object allDexElements = combineArray(newDexElements, baseDexElements);
        Object pathList = getPathList(getPathClassLoader());
        ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);
    }

根据传入的dex的文件目录defaultDexOptPath,构造DexClassLoader对象dexClassLoader,然后通过getDexElements方法获得原来的dexElements对象,之后拿到dexClassLoader对象中的dexElements对象,调用combineArray方法将这两个对象进行结合,将我们传进来的dex插到该对象的最前面,之后调用ReflectionUtils.setField()方法,将dexElements进行替换。combineArray方法中做的就是扩展数组,将第二个数组插入到第一个数组的最前面

private static Object combineArray(Object firstArray, Object secondArray) {
        Class localClass = firstArray.getClass().getComponentType();
        int firstArrayLength = Array.getLength(firstArray);
        int allLength = firstArrayLength + Array.getLength(secondArray);
        Object result = Array.newInstance(localClass, allLength);
        for (int k = 0; k < allLength; ++k) {
            if (k < firstArrayLength) {
                Array.set(result, k, Array.get(firstArray, k));
            } else {
                Array.set(result, k, Array.get(secondArray, k - firstArrayLength));
            }
        }
        return result;
    }

之后如果你有补丁要应用,直接调用Nuwa.loadPatch()方法,传入补丁的目录,重启应用之后就可以进行热更新了。这是Nuwa应用层的实现,可以看到,并不复杂。相对复杂的是Gradle插件层的实现。Gradle插件要做的事就是拿到所有class,在其构造函数中注入Hack.class,使其直接引用另一个dex中的文件,防止被打上ISPREVERIFIED标记。并且混淆的时候要应用上一次release版本的mapping文件。现在有两点关键内容:

如何拿到所有的class 如何在构造函数中注入代码

我们先来解决第二点,如何注入代码,Nuwa使用的是asm注入代码。

现在假设我们已经存在了hack.apk,并且里面已经有了Hack.class文件,其源代码如下

package cn.edu.zafu.hotpatch.asm;

/**
 * @author lizhangqu
 * @since 2016-03-06 10:31
 */
public class Hack {
}

我们编写一个测试类Test,里面有一个测试方法,我们需要将Hack.class注入到Test的构造函数中,让其直接引用另一个dex中的类。

public class Test {
    public void method1(){
        String str="111";
    }
}

我们编译一下,得到Test.clss,将其复制到一个目录dir。然后终端进入到该目录,使用javap命令查看字节码

这里写图片描述

可以看到图中有 < init >字样,该处就是构造函数,然后看到4:return,这是构造函数的结束的地方。现在我们读入该文件,并对其进行字节码修改,然后写入该目录下dest目录下。在这之前,需要加入asm的依赖,至于asm的使用,请自行查询。

 compile 'org.ow2.asm:asm:5.0.4'

我们先将该文件读入,获得输入流,调用referHackWhenInit方法,将输入流传入,用ClassVisitor对象访问该对象,实现MethodVisitor方法,在该方法中访问对象中的方法,对方法名进行判断,如果是构造函数,则对其进行字节码注入操作,接下来运行main方法,查看dest目录下生成的文件。

public class Main {

    public static void main(String[] args) throws IOException {
        File srcFile = new File("/Users/lizhangqu/AndroidStudioProjects/Hotpatch/bak/Test.class");
        File destDir = new File("/Users/lizhangqu/AndroidStudioProjects/Hotpatch/bak/dest/");

        if (!destDir.exists()) {
            destDir.mkdirs();
        }
        InputStream is = new FileInputStream(srcFile);
        byte[] bytes = referHackWhenInit(is);


        File destFile = new File(destDir, "Test.class");
        FileOutputStream fos = new FileOutputStream(destFile);
        fos.write(bytes);
        fos.close();

    }

    private static byte[] referHackWhenInit(InputStream inputStream) throws IOException {
        ClassReader cr = new ClassReader(inputStream);
        ClassWriter cw = new ClassWriter(cr, 0);
        ClassVisitor cv = new ClassVisitor(Opcodes.ASM4, cw) {
            @Override
            public MethodVis



 
分享到:QQ空间新浪微博腾讯微博微信百度贴吧QQ好友复制网址打印

您可能想查找下面的文章:

  • 聊聊Android 热修复Nuwa有哪些坑
  • Android 热修复使用Gradle Plugin1.5改造Nuwa插件
  • Android 热修复原理及Gradle插件源码解析(以Nuwa为例)

相关文章

  • 2017-05-26安卓图片滑动,实现带小点的导航页面效果,安卓小点
  • 2017-05-26Android 如何保证service在后台不被kill,androidkill
  • 2017-05-222.5.0 构建一个可复用的自定义BaseAdapter
  • 2017-05-26集成websocket即时通讯 java聊天源码 代码下载 java后台框架源码 websocket源码 IM,websocket即时通讯
  • 2017-05-26安卓四大组件之服务,安卓四大组件
  • 2017-05-26android开发环境以及genymotion虚拟机配合HBuilder测试(自总结),genymotionhbuilder
  • 2017-05-26React Native Android入门实战及深入源码分析系列(2)——React Native源码编译
  • 2017-05-26安卓自定义view(简单折线图),安卓自定义view折线
  • 2017-05-224.4.1 ContentProvider初探
  • 2017-05-26Android移动APP开发笔记——Cordova(PhoneGap)通过CordovaPlugin插件调用 Activity 实例,phonegapcordova

文章分类

  • JavaScript
  • ASP.NET
  • PHP
  • 正则表达式
  • AJAX
  • JSP
  • ASP
  • Flex
  • XML
  • 编程技巧
  • Android
  • swift
  • C#教程
  • vb
  • vb.net
  • C语言
  • Java
  • Delphi
  • 易语言
  • vc/mfc
  • 嵌入式开发
  • 游戏开发
  • ios
  • 编程问答
  • 汇编语言
  • 微信小程序
  • 数据结构
  • OpenGL
  • 架构设计
  • qt
  • 微信公众号

最近更新的内容

    • Bottom Sheet
    • Android Studio快捷键,androidstudio
    • Android开发学习——Android项目的目录结构,android项目
    • BaseAdapter的使用(笔记)
    • Handler消息传递机制(二)Handler,Loop,Message,MessageQueue的工作原理
    • Android中通信协议,Android通信协议
    • Android中Activity运行时屏幕方向与显示方式详解,androidactivity
    • 「视频直播技术详解」系列之七:直播云 SDK 性能测试模型,sdk性能测试
    • 「视频直播技术详解」系列之六:现代播放器原理,
    • Linux设备模型之input子系统详解

关于我们 - 联系我们 - 免责声明 - 网站地图

©2020-2025 All Rights Reserved. linkedu.com 版权所有