android 原理 更新 热_android热更新原理 - CSDN
  • Android热更新实现原理

    2015-11-15 01:31:22
    最近Android社区的氛围很不错嘛,连续放出一系列的Android动态加载插件和热更新库,这篇文章就来介绍一下Android中实现热更新原理。ClassLoader我们知道Java在运行时加载对应的类是通过ClassLoader来实现的,...

    最近Android社区的氛围很不错嘛,连续放出一系列的Android动态加载插件和热更新库,这篇文章就来介绍一下Android中实现热更新的原理。

    ClassLoader

    我们知道Java在运行时加载对应的类是通过ClassLoader来实现的,ClassLoader本身是一个抽象来,Android中使用PathClassLoader类作为Android的默认的类加载器,
    PathClassLoader其实实现的就是简单的从文件系统中加载类文件。PathClassLoade本身继承自BaseDexClassLoader,BaseDexClassLoader重写了findClass方法,
    该方法是ClassLoader的核心

    @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;
    }

    看源码可知,BaseDexClassLoader将findClass方法委托给了pathList对象的findClass方法,pathList对象是在BaseDexClassLoader的构造函数中new出来的,
    它的类型是DexPathList。看下DexPathList.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;
    }

    直接就是遍历dexElements列表,然后通过调用element.dexFile对象上的loadClassBinaryName方法来加载类,如果返回值不是null,就表示加载类成功,会将这个Class对象返回。
    而dexElements对象是在DexPathList类的构造函数中完成初始化的。

    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                               suppressedExceptions);

    makeDexElements所做的事情就是遍历我们传递来的dexPath,然后一次加载每个dex文件。

    实现

    上面分析了Android中的类的加载的流程,可以看出来DexPathList对象中的dexElements列表是类加载的一个核心,一个类如果能被成功加载,那么它的dex一定
    会出现在dexElements所对应的dex文件中,并且dexElements中出现的顺序也很重要,在dexElements前面出现的dex会被优先加载,一旦Class被加载成功,
    就会立即返回,也就是说,我们的如果想做hotpatch,一定要保证我们的hotpacth dex文件出现在dexElements列表的前面。

    要实现热更新,就需要我们在运行时去更改PathClassLoader.pathList.dexElements,由于这些属性都是private的,因此需要通过反射来修改。另外,构造我们自己的dex文件
    所对应的dexElements数组的时候,我们也可以采取一个比较取巧的方式,就是通过构造一个DexClassLoader对象来加载我们的dex文件,并且调用一次dexClassLoader.loadClass(dummyClassName);
    方法,这样,dexClassLoader.pathList.dexElements中,就会包含我们的dex,通过把dexClassLoader.pathList.dexElements插入到系统默认的classLoader.pathList.dexElements列表前面,就可以让系统优先加载我们的dex中的类,从而可以实现热更新了。下面展示一部分代码

    private static synchronized Boolean injectAboveEqualApiLevel14(
                String dexPath, String defaultDexOptPath, String nativeLibPath, String dummyClassName) {
        Log.i(TAG, "--> injectAboveEqualApiLevel14");
        PathClassLoader pathClassLoader = (PathClassLoader) DexInjector.class.getClassLoader();
        DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, nativeLibPath, pathClassLoader);
        try {
            dexClassLoader.loadClass(dummyClassName);
            Object dexElements = combineArray(
                    getDexElements(getPathList(pathClassLoader)),
                    getDexElements(getPathList(dexClassLoader)));
    
    
            Object pathList = getPathList(pathClassLoader);
            setField(pathList, pathList.getClass(), "dexElements", dexElements);
        } catch (Throwable e) {
            e.printStackTrace();
            return false;
        }
        Log.i(TAG, "<-- injectAboveEqualApiLevel14 End.");
        return true;
    }

    完整的demo请参考我的GitHub

    上面只是说了一下hotpatch的原理,具体实现的时候,如果你的app应用了multidex,还会遇到其他的坑,请参考:

    展开全文
  • 第一部分重点是将当下热门的热更新方案实现之后再研究,第二部分则是自己动手实现一个自己的热更新框架。 Android热更新技术的研究与实现之研究篇 ———概念讲解——– 热更新 相关概念 这个词出现的时间已经很...

    第一部分重点是将当下热门的热更新方案实现之后再研究,第二部分则是自己动手实现一个自己的热更新框架。

    Android热更新技术的研究与实现之研究篇

    ———概念讲解——–

    热更新 相关概念

    这个词出现的时间已经很久了,感觉现在要找工作才来看是晚了不少,但是好东西什么时候学习都不晚的。 

    今天看到一句话,和大家分享下,人一生有三样东西是别人抢不走的:

    吃进胃里的食物

    藏在心中的梦想

    读进大脑里的书

    所以趁着我们的时光正好,多学点东西肯定是赚翻的!!(当然多吃点也没错,不配点图感觉好突兀) 

    言归正传,首先我们要了解与热更新相关的一些概念吧!

    组件化—-就是将一个app分成多个模块,每个模块都是一个组件(Module),开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件合并统一成一个apk,这就是组件化开发。我之前的开发方式基本上都是这一种。具体可以参考Android组件化方案

    插件化–将整个app拆分成很多模块,这些模块包括一个宿主和多个插件,每个模块都是一个apk(组件化的每个模块是个lib),最终打包的时候将宿主apk和插件apk分开或者联合打包。开发中,往往会堆积很多的需求进项目,超过 65535 后,插件化就是一个解决方案。

    放张图帮大家理解:

    热更新 – 更新的类或者插件粒度较小的时候,我们会称之为热修复,一般用于修复bug!!比如更新一个bug方法或者紧急修改lib包,甚至一个类等。2016 Google 的 Android Studio 推出了Instant Run 功能 同时提出了3个名词;

    热部署” – 方法内的简单修改,无需重启app和Activity。 “暖部署” – app无需重启,但是activity需要重启,比如资源的修改。 “冷部署” – app需要重启,比如继承关系的改变或方法的签名变化等。 

    所以站在app开发者角度的“热”是指在不发版的情况来实现更新,而Google提出的“热”是指值无需重新启动。 同时在开发插件化的时候也有两种情景,一种是插件与宿主apk没有交互,只是在用户使用到的时候进行一次吊起,还有一种是与宿主有很多的交互。

    增量更新,与热更新区别最大的一个,其实这个大家应该很好理解,安卓上的有些很大的应用,特别是游戏,大则好几个G的多如牛毛,但是每次更新的时候却不是要去下载最新版,而只是下载一个几十兆的增量包就可以完成更新了,而这所使用的技术就是增量更新了。实现的过程大概是这个样子的:我们手机上安装着某个大应用,下载增量包之后,手机上的apk和增量包合并形成新的包,然后会再次安装,这个安装过程可能是可见的,或者应用本身有足够的权限直接在后台安装完成。

    今天碰到Android Studio的更新,这应该就是增量更新啦!补丁包只有51M,如果下载新版本有1G多。

    而热更新究竟是什么呢?

    有一些这样的情况, 当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和重新发布。老是发布版本用户会疯掉的!!!(好吧 猿猿们也会疯掉。。)

     

    这时候就提出一个问题:有没有办法以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载,覆盖安装?

    这种需要替换运行时新的类和资源文件的加载,就可以认为是热操作了。而在热更新出现之前,通过反射注解、反射调用和反射注入等方式已经可以实现类的动态加载了。而热更新框架的出现就是为了解决这样一个问题的。

    从某种意义上来说,热更新就是要做一件事,替换。当替换的东西属于大块内容的时候,就是模块化了,当你去替换方法的时候,叫热更新,当你替换类的时候,加热插件,而且重某种意义上讲,所有的热更新方案,都是一种热插件,因为热更新方案就是在app之外去干这个事。就这么简单的理解。无论是替换一个类,还是一个方法,都是在干替换这件事请。。这里的替换,也算是几种hook操作,无论在什么代码等级上,都是一种侵入性的操作。

    所以总结一句话简单理解热更新 HotFix 就是改变app运行行为的技术!(或者说就是对已发布app进行bug修复的技术) 此时的猿猿们顿时眼前一亮,用户也笑了。。

    好的,现在我们已经知道热更新为何物了,那么我们就先看看热更新都有哪些成熟的方案在使用了。

    在我们写好的安卓项目中,有很多逻辑代码,在预编译和编译阶段互相连在一起,各种业务逻辑的链接和lib的链接,各种变量和运算符的的编译优化;

    热更新方案介绍

    热更新方案发展至今,有很多团队开发过不同的解决方案,包括Dexposed、AndFix,(HotFix)Sophix,Qzone超级补丁的类Nuwa方式,微信的Tinker, 大众点评的nuwa、百度金融的rocooFix, 饿了么的amigo以及美团的robust、腾讯的Bugly热更新。 

    苹果公司现在已经禁止了热更新,不过估计也阻止不了开发者们的热情吧!

    我先讲几种方案具体如何使用,说下原理,最后再讲如何实现一个自己的热更新方案!

    –Dexposed & AndFix & (HotFix)SopHix –阿里热更新方案

    Dexposed

    “Dexposed”是大厂阿里以前的一个开源热更新项目,基于Xposed“Xposed”的AOP框架,方法级粒度,可以进行AOP编程、插桩、热补丁、SDK hook等功能。

    Xposeed大家如果不熟悉的话可以在网上搜一下” Xposed源码剖析——概述”,我以前用Xposed做过一些小东西(其实就是获取root权限后hook修改一些手机数据,比如支付宝步数,qq微信步数等,当然了,余额啥的是改不了的),在这里就不献丑了,毕竟重点也不是这个。我们可以看出Xposed有一个缺陷就是需要root,而Dexposed就是一个不需要root权限的hook框架。目前阿里系主流app例如手机淘宝,支付宝,天猫都使用了Dexposed支持在线热更新。

    Dexposed中的AOP原理来自于Xposed。在Dalvik虚拟机下,主要是通过改变一个方法对象方法在Dalvik虚拟机中的定 义来实现,具体做法就是将该方法的类型改变为native并且将这个方法的实现链接到一个通用的Native Dispatch方法上。这个 Dispatch方法通过JNI回调到Java端的一个统一处理方法,最后在统一处理方法中调用before, after函数来实现AOP。在Art虚拟机上目前也是是通过改变一个 ArtMethod的入口函数来实现。

    android4.4之后的版本都用Art取代了Dalvik,所以要hook Android4.4以后的版本就必须去适配Art虚拟机的机制。目前官方表示,为了适配Art的dexposed_l只是beta版,所以最好不要在正式的线上产品中使用它。

    Dexposed已经被抛弃,原因很明显,4.4以后不支持了,我们就不细细分析这个方案了,感兴趣的朋友可以通过“这里”了解。简单讲下它的实现方式:

    引入一个名为patchloader的jar包,这个函数库实现了一个热更新框架,宿主apk(可能含有bug的上线版本)在发布时会将这个jar包一起打包进apk中

    补丁apk(已修复线上版本bug的版本)只是在编译时需要这个jar包,但打包成apk时不包含这个jar包,以免补丁apk集成到宿主apk中时发生冲突

    补丁apk将会以provided的形式依赖dexposedbridge.jar和patchloader.jar

    通过在线下载的方式从服务器下载补丁apk,补丁apk集成到宿主apk中,使用补丁apk中的函数替换原来的函数,从而实现在线修复bug的功能。

    AndFix

    AndFix是一个Android App的在线热补丁框架。使用此框架,我们能够在不重复发版的情况下,在线修改App中的Bug。AndFix就是 “Android Hot-Fix”的缩写。支持Android 2.3到6.0版本,并且支持arm与X86系统架构的设备。完美支持Dalvik与ART的Runtime。AndFix 的补丁文件是以 .apatch 结尾的文件。它从你的服务器分发到你的客户端来修复你App的bug 。

    AndFix的实现方式(画的丑勿怪⊙﹏⊙):

    首先添加依赖

    compile ‘com.alipay.euler:andfix:0.3.1@aar’

    然后在Application.onCreate() 中添加以下代码

    patchManager = new PatchManager(context);

    patchManager.init(appversion);//current version

    patchManager.loadPatch();

    可以用这句话获取appversion,每次appversion变更都会导致所有补丁被删除,如果appversion没有改变,则会加载已经保存的所有补丁。

    String appversion= getPackageManager().getPackageInfo(getPackageName(), 0).versionName;

    然后在需要的地方调用PatchManager的addPatch方法加载新补丁,比如可以在下载补丁文件之后调用。

    之后就是打补丁的过程了,首先生成一个apk文件,然后更改代码,在修复bug后生成另一个apk。通过官方提供的工具apkpatch生成一个.apatch格式的补丁文件,需要提供原apk,修复后的apk,以及一个签名文件。

    通过网络传输或者adb push的方式将apatch文件传到手机上,然后运行到addPatch的时候就会加载补丁。

    AndFix更新的原理:

    首先通过虚拟机的JarFile加载补丁文件,然后读取PATCH.MF文件得到补丁类的名称

    使用DexFile读取patch文件中的dex文件,得到后根据注解来获取补丁方法,然后根据注解中得到类名和方法名,使用classLoader获取到Class,然后根据反射得到bug方法。

    jni层使用C++的指针替换bug方法对象的属性来修复bug。

    具体的代码主要都是我们在Application中初始化的PatchManager中。

    public PatchManager(Context context) {

        mContext = context;

        mAndFixManager = new AndFixManager(mContext);//初始化AndFixManager

        mPatchDir = new File(mContext.getFilesDir(), DIR);//初始化存放patch补丁文件的文件夹

        mPatchs = new ConcurrentSkipListSet();//初始化存在Patch类的集合,此类适合大并发

        mLoaders = new ConcurrentHashMap();//初始化存放类对应的类加载器集合

    }

    其中mAndFixManager = new AndFixManager(mContext);//初始化AndFixManager:

    public AndFixManager(Context context) {

        mContext = context;

        mSupport = Compat.isSupport();//判断Android机型是否适支持AndFix

        if (mSupport) {

            mSecurityChecker = new SecurityChecker(mContext);//初始化签名判断类

            mOptDir = new File(mContext.getFilesDir(), DIR);//初始化patch文件存放的文件夹

            if (!mOptDir.exists() && !mOptDir.mkdirs()) {// make directory fail

                mSupport = false;

                Log.e(TAG, "opt dir create error.");

            } else if (!mOptDir.isDirectory()) {// not directory

                mOptDir.delete();//如果不是文件目录就删除

                mSupport = false;

            }

        }

    }

    。。。。。。。。。。。。

    然后是对版本的初始化mPatchManager.init(appversion)init(String appVersion)代码如下:

    public void init(String appVersion) {

        if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail

            Log.e(TAG, "patch dir create error.");

            return;

        } else if (!mPatchDir.isDirectory()) {// not directory

            mPatchDir.delete();

            return;

        }

        SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,

                Context.MODE_PRIVATE);//存储关于patch文件的信息

        //根据你传入的版本号和之前的对比,做不同的处理

        String ver = sp.getString(SP_VERSION, null);

        if (ver == null || !ver.equalsIgnoreCase(appVersion)) {

            cleanPatch();//删除本地patch文件

            sp.edit().putString(SP_VERSION, appVersion).commit();//并把传入的版本号保存

        } else {

            initPatchs();//初始化patch列表,把本地的patch文件加载到内存

        }

    }

    /*************省略初始化、删除、加载具体方法实现*****************/

    init初始化主要是对patch补丁文件信息进行保存或者删除以及加载。

    那么patch补丁文件是如何加载的呢?其实patch补丁文件本质上是一个jar包,使用JarFile来读取即可:

    public Patch(File file) throws IOException {

        mFile = file;

        init();

    }

    @SuppressWarnings("deprecation")

    private void init() throws IOException {

        JarFile jarFile = null;

        InputStream inputStream = null;

        try {

            jarFile = new JarFile(mFile);//使用JarFile读取Patch文件

            JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);//获取META-INF/PATCH.MF文件

            inputStream = jarFile.getInputStream(entry);

            Manifest manifest = new Manifest(inputStream);

            Attributes main = manifest.getMainAttributes();

            mName = main.getValue(PATCH_NAME);//获取PATCH.MF属性Patch-Name

            mTime = new Date(main.getValue(CREATED_TIME));//获取PATCH.MF属性Created-Time

            mClassesMap = new HashMap>();

            Attributes.Name attrName;

            String name;

            List strings;

            for (Iterator it = main.keySet().iterator(); it.hasNext();) {

                attrName = (Attributes.Name) it.next();

                name = attrName.toString();

                //判断name的后缀是否是-Classes,并把name对应的值加入到集合中,对应的值就是class类名的列表

                if (name.endsWith(CLASSES)) {

                    strings = Arrays.asList(main.getValue(attrName).split(","));

                    if (name.equalsIgnoreCase(PATCH_CLASSES)) {

                        mClassesMap.put(mName, strings);

                    } else {

                        mClassesMap.put(

                                name.trim().substring(0, name.length() - 8),// remove

                                                                            // "-Classes"

                                strings);

                    }

                }

            }

        } finally {

            if (jarFile != null) {

                jarFile.close();

            }

            if (inputStream != null) {

                inputStream.close();

            }

        }

    }

    然后就是最重要的patchManager.loadPatch()

    public void loadPatch() {

        mLoaders.put("*", mContext.getClassLoader());// wildcard

        Set patchNames;

        List classes;

        for (Patch patch : mPatchs) {

            patchNames = patch.getPatchNames();

            for (String patchName : patchNames) {

                classes = patch.getClasses(patchName);//获取patch对应的class类的集合List

                mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),

                        classes);//修复bug方法

            }

        }

    }

    循环获取补丁对应的class类来修复bug方法,mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),classes)

    public synchronized void fix(File file, ClassLoader classLoader,

            List classes) {

        if (!mSupport) {

            return;

        }

        //判断patch文件的签名

        if (!mSecurityChecker.verifyApk(file)) {// security check fail

            return;

        }

            /******省略部分代码********/

            //加载patch文件中的dex

            final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),

                    optfile.getAbsolutePath(), Context.MODE_PRIVATE);

            if (saveFingerprint) {

                mSecurityChecker.saveOptSig(optfile);

            }

            ClassLoader patchClassLoader = new ClassLoader(classLoader) {

                @Override

                protected Class findClass(String className)

                        throws ClassNotFoundException {//重写ClasLoader的findClass方法

                    Class clazz = dexFile.loadClass(className, this);

                    if (clazz == null

                            && className.startsWith("com.alipay.euler.andfix")) {

                        return Class.forName(className);// annotation’s class

                                                        // not found

                    }

                    if (clazz == null) {

                        throw new ClassNotFoundException(className);

                    }

                    return clazz;

                }

            };

            Enumeration entrys = dexFile.entries();

            Class clazz = null;

            while (entrys.hasMoreElements()) {

                String entry = entrys.nextElement();

                if (classes != null && !classes.contains(entry)) {

                    continue;// skip, not need fix

                }

                clazz = dexFile.loadClass(entry, patchClassLoader);//获取有bug的类文件

                if (clazz != null) {

                    fixClass(clazz, classLoader);// next code

                }

            }

        } catch (IOException e) {

            Log.e(TAG, "pacth", e);

      }

    }

    private void fixClass(Class clazz, ClassLoader classLoader) {

        Method[] methods = clazz.getDeclaredMethods();

        MethodReplace methodReplace;

        String clz;

        String meth;

        for (Method method : methods) {

            //获取此方法的注解,因为有bug的方法在生成的patch的类中的方法都是有注解的

            methodReplace = method.getAnnotation(MethodReplace.class);

            if (methodReplace == null)

                continue;

            clz = methodReplace.clazz();//获取注解中clazz的值

            meth = methodReplace.method();//获取注解中method的值

            if (!isEmpty(clz) && !isEmpty(meth)) {

                replaceMethod(classLoader, clz, meth, method);//next code

            }

        }

    }

    private void replaceMethod(ClassLoader classLoader, String clz,

            String meth, Method method) {

        try {

            String key = clz + "@" + classLoader.toString();

            Class clazz = mFixedClass.get(key);//判断此类是否被fix

            if (clazz == null) {// class not load

                Class clzz = classLoader.loadClass(clz);

                // initialize target class

                clazz = AndFix.initTargetClass(clzz);//初始化class

            }

            if (clazz != null) {// initialize class OK

                mFixedClass.put(key, clazz);

                Method src = clazz.getDeclaredMethod(meth,

                        method.getParameterTypes());//根据反射获取到有bug的类的方法(有bug的apk)

                AndFix.addReplaceMethod(src, method);//src是有bug的方法,method是补丁方法

            }

        } catch (Exception e) {

            Log.e(TAG, "replaceMethod", e);

      }

    }

    public static void addReplaceMethod(Method src, Method dest) {

        try {

            replaceMethod(src, dest);//调用了native方法

            initFields(dest.getDeclaringClass());

        } catch (Throwable e) {

            Log.e(TAG, "addReplaceMethod", e);

        }

    }

    private static native void replaceMethod(Method dest, Method src);

    从上面的bug修复源码可以看出,就是在找补丁包中有@MethodReplace注解的方法,然后反射获取原apk中方法的位置,最后进行替换。

    而最后调用的replaceMethod(Method dest,Method src)则是native方法,源码中有两个replaceMethod:

    extern void dalvik_replaceMethod(JNIEnv* env, jobject src, jobject dest);//Dalvik

    extern void art_replaceMethod(JNIEnv* env, jobject src, jobject dest);//Art

    从源码的注释也能看出来,因为安卓4.4版本之后使用的不再是Dalvik虚拟机,而是Art虚拟机,所以需要对不同的手机系统做不同的处理。

    首先看Dalvik替换方法的实现:

    extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(

        JNIEnv* env, jobject src, jobject dest) {

        jobject clazz = env->CallObjectMethod(dest, jClassMethod);

        ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(

            dvmThreadSelf_fnPtr(), clazz);

        clz->status = CLASS_INITIALIZED;

        Method* meth = (Method*) env->FromReflectedMethod(src);

        Method* target = (Method*) env->FromReflectedMethod(dest);

        LOGD("dalvikMethod: %s", meth->name);

        meth->jniArgInfo = 0x80000000;

        meth->accessFlags |= ACC_NATIVE;//把Method的属性设置成Native方法

        int argsSize = dvmComputeMethodArgsSize_fnPtr(meth);

        if (!dvmIsStaticMethod(meth))

        argsSize++;

        meth->registersSize = meth->insSize = argsSize;

        meth->insns = (void*) target;

        meth->nativeFunc = dalvik_dispatcher;//把方法的实现替换成native方法

    }

    Art替换方法的实现:

    //不同的art系统版本不同处理也不同

    extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod(

            JNIEnv* env, jobject src, jobject dest) {

        if (apilevel > 22) {

            replace_6_0(env, src, dest);

        } else if (apilevel > 21) {

            replace_5_1(env, src, dest);

        } else {

            replace_5_0(env, src, dest);

        }

    }

    //以5.0为例:

    void replace_5_0(JNIEnv* env, jobject src, jobject dest) {

        art::mirror::ArtMethod* smeth =

                (art::mirror::ArtMethod*) env->FromReflectedMethod(src);

        art::mirror::ArtMethod* dmeth =

                (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);

        dmeth->declaring_class_->class_loader_ =

                smeth->declaring_class_->class_loader_; //for plugin classloader

        dmeth->declaring_class_->clinit_thread_id_ =

                smeth->declaring_class_->clinit_thread_id_;

        dmeth->declaring_class_->status_ = (void *)((int)smeth->declaring_class_->status_-1);

        //把一些参数的指针给补丁方法

        smeth->declaring_class_ = dmeth->declaring_class_;

        smeth->access_flags_ = dmeth->access_flags_;

        smeth->frame_size_in_bytes_ = dmeth->frame_size_in_bytes_;

        smeth->dex_cache_initialized_static_storage_ =

                dmeth->dex_cache_initialized_static_storage_;

        smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;

        smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;

        smeth->vmap_table_ = dmeth->vmap_table_;

        smeth->core_spill_mask_ = dmeth->core_spill_mask_;

        smeth->fp_spill_mask_ = dmeth->fp_spill_mask_;

        smeth->mapping_table_ = dmeth->mapping_table_;

        smeth->code_item_offset_ = dmeth->code_item_offset_;

        smeth->entry_point_from_compiled_code_ =

                dmeth->entry_point_from_compiled_code_;

        smeth->entry_point_from_interpreter_ = dmeth->entry_point_from_interpreter_;

        smeth->native_method_ = dmeth->native_method_;//把补丁方法替换掉

        smeth->method_index_ = dmeth->method_index_;

        smeth->method_dex_index_ = dmeth->method_dex_index_;

        LOGD("replace_5_0: %d , %d", smeth->entry_point_from_compiled_code_,

                dmeth->entry_point_from_compiled_code_);

    }

    其实这个替换过程可以看做三步完成

    打开链接库得到操作句柄,获取native层的内部函数,得到ClassObject对象

    修改访问权限的属性为public

    得到新旧方法的指针,新方法指向目标方法,实现方法的替换。

    如果我们想知道补丁包中到底替换了哪些方法,可以直接方便易patch文件,然后看到的所有含有@ReplaceMethod注解的方法基本上就都是需要替换的方法了。

    最近我在学习C++,顿时感觉到还是这种可以控制底层的语言是多么强大,不过Java可以调用C++,也就没什么可吐槽的了!

    好的,现在AndFix我们分析了一遍它的实现过程和原理,其优点是不需要重启即可应用补丁,遗憾的是它还是有不少缺陷的,这直接导致阿里再次抛弃了它,缺陷如下:

    并不能支持所有的方法修复

    不支持YunOS

    无法添加新类和新的字段

    需要使用加固前的apk制作补丁,但是补丁文件很容易被反编译,也就是修改过的类源码容易泄露。

    使用加固平台可能会使热补丁功能失效(看到有人在360加固提了这个问题,自己还未验证)。

    链接:https://www.jianshu.com/p/4ecd611383e6
     

    展开全文
  • 如果只是会这些修复框架的使用那意义并不大,我们还需要了解它们的原理,这样不管修复框架如何变化,只要基本原理不变,我们就可以很快的掌握它们。这一个系列不会对某些修复框架源码进行解析,...

    title: “Android热修复原理(一)热修复框架对比和代码修复”
    date: 2018-03-12 00:18
    photos:

    • https://s2.ax1x.com/2019/05/29/VnQrOx.png
      tag:
    • Android热修复原理
      categories:
    • Android应用层

    本文首发于微信公众号「后场村刘皇叔」

    关联系列
    解析ClassLoader系列

    前言

    在Android应用开发中,热修复技术被越来越多的开发者所使用,也出现了很多热修复框架,比如:AndFix、Tinker、Dexposed和Nuwa等等。如果只是会这些热修复框架的使用那意义并不大,我们还需要了解它们的原理,这样不管热修复框架如何变化,只要基本原理不变,我们就可以很快的掌握它们。这一个系列不会对某些热修复框架源码进行解析,而是讲解热修复框架的通用原理。

    1.热修复的产生概述

    在开发中我们会遇到如下的情况:

    1. 刚发布的版本出现了严重的bug,这就需要去解决bug、测试并打渠道包在各个应用市场上重新发布,这会耗费大量的人力物力,代价会比较大。
    2. 已经改正了此前发布版本的bug,如果下一个版本是一个大版本,那么两个版本的间隔时间会很长,这样要等到下个大版本发布再修复bug,这样此前版本的bug会长期的影响用户。
    3. 版本升级率不高,并且需要很长时间来完成版本覆盖,此前版本的bug就会一直影响不升级版本的用户。
    4. 有一个小而重要的功能,需要短时间内完成版本覆盖,比如节日活动。

    为了解决上面的问题,热修复框架就产生了。对于Bug的处理,开发人员不要过于依赖热修复框架,在开发的过程中还是要按照标准的流程做好自测、配合测试人员完成测试流程。

    2.热修复框架的对比

    热修复框架的种类繁多,按照公司团队划分主要有以下几种:

    类别 成员
    阿里系 AndFix、Dexposed、阿里百川、Sophix
    腾讯系 微信的Tinker、QQ空间的超级补丁、手机QQ的QFix
    知名公司 美团的Robust、饿了么的Amigo、美丽说蘑菇街的Aceso
    其他 RocooFix、Nuwa、AnoleFix

    虽然热修复框架很多,但热修复框架的核心技术主要有三类,分别是代码修复、资源修复和动态链接库修复,其中每个核心技术又有很多不同的技术方案,每个技术方案又有不同的实现,另外这些热修复框架仍在不断的更新迭代中,可见热修复框架的技术实现是繁多可变的。作为开发需需要了解这些技术方案的基本原理,这样就可以以不变应万变。

    部分热修复框架的对比如下表所示。

    特性 AndFix Tinker/Amigo QQ空间 Robust/Aceso
    即时生效
    方法替换
    类替换
    类结构修改
    资源替换
    so替换
    支持gradle
    支持ART
    支持Android7.0

    我们可以根据上表和具体业务来选择合适的热修复框架,当然上表的信息很难做到完全准确,因为部分的热修复框架还在不断更新迭代。
    从表中也可以发现Tinker和Amigo拥有的特性最多,是不是就选它们呢?也不尽然,拥有的特性多也意味着框架的代码量庞大,我们需要根据业务来选择最合适的,假设我们只是要用到方法替换,那么使用Tinker和Amigo显然是大材小用了。另外如果项目需要即时生效,那么使用Tinker和Amigo是无法满足需求的。对于即时生效,AndFix、Robust和Aceso都满足这一点,这是因为AndFix的代码修复采用了底层替换方案,而Robust和Aceso的代码修复借鉴了Instant Run原理,现在我们就来学习代码修复。

    3.代码修复

    代码修复主要有三个方案,分别是底层替换方案、类加载方案和Instant Run方案。

    3.1 类加载方案

    类加载方案基于Dex分包方案,什么是Dex分包方案呢?这个得先从65536限制和LinearAlloc限制说起。
    65536限制
    随着应用功能越来越复杂,代码量不断地增大,引入的库也越来越多,可能会在编译时提示如下异常:

    com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536
    

    这说明应用中引用的方法数超过了最大数65536个。产生这一问题的原因就是系统的65536限制,65536限制的主要原因是DVM Bytecode的限制,DVM指令集的方法调用指令invoke-kind索引为16bits,最多能引用 65535个方法。
    LinearAlloc限制
    在安装时可能会提示INSTALL_FAILED_DEXOPT。产生的原因就是LinearAlloc限制,DVM中的LinearAlloc是一个固定的缓存区,当方法数过多超出了缓存区的大小时会报错。

    为了解决65536限制和LinearAlloc限制,从而产生了Dex分包方案。Dex分包方案主要做的是在打包时将应用代码分成多个Dex,将应用启动时必须用到的类和这些类的直接引用类放到主Dex中,其他代码放到次Dex中。当应用启动时先加载主Dex,等到应用启动后再动态的加载次Dex,从而缓解了主Dex的65536限制和LinearAlloc限制。

    Dex分包方案主要有两种,分别是Google官方方案、Dex自动拆包和动态加载方案。因为Dex分包方案不是本章的重点,这里就不再过多的介绍,我们接着来学习类加载方案。
    Android解析ClassLoader(二)Android中的ClassLoader中讲到了ClassLoader的加载过程,其中一个环节就是调用DexPathList的findClass的方法,如下所示。
    libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

     public Class<?> findClass(String name, List<Throwable> suppressed) {
            for (Element element : dexElements) {//1
                Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
                if (clazz != null) {
                    return clazz;
                }
            }
            if (dexElementsSuppressedExceptions != null) {
                suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
            }
            return null;
        }
    

    Element内部封装了DexFile,DexFile用于加载dex文件,因此每个dex文件对应一个Element。
    多个Element组成了有序的Element数组dexElements。当要查找类时,会在注释1处遍历Element数组dexElements(相当于遍历dex文件数组),注释2处调用Element的findClass方法,其方法内部会调用DexFile的loadClassBinaryName方法查找类。如果在Element中(dex文件)找到了该类就返回,如果没有找到就接着在下一个Element中进行查找。
    根据上面的查找流程,我们将有bug的类Key.class进行修改,再将Key.class打包成包含dex的补丁包Patch.jar,放在Element数组dexElements的第一个元素,这样会首先找到Patch.dex中的Key.class去替换之前存在bug的Key.class,排在数组后面的dex文件中的存在bug的Key.class根据ClassLoader的双亲委托模式就不会被加载,这就是类加载方案,如下图所示。
    VnQD61.png

    类加载方案需要重启App后让ClassLoader重新加载新的类,为什么需要重启呢?这是因为类是无法被卸载的,因此要想重新加载新的类就需要重启App,因此采用类加载方案的热修复框架是不能即时生效的。
    虽然很多热修复框架采用了类加载方案,但具体的实现细节和步骤还是有一些区别的,比如QQ空间的超级补丁和Nuwa是按照上面说得将补丁包放在Element数组的第一个元素得到优先加载。微信Tinker将新旧apk做了diff,得到patch.dex,然后将patch.dex与手机中apk的classes.dex做合并,生成新的classes.dex,然后在运行时通过反射将classes.dex放在Element数组的第一个元素。饿了么的Amigo则是将补丁包中每个dex 对应的Element取出来,之后组成新的Element数组,在运行时通过反射用新的Element数组替换掉现有的Element 数组。

    采用类加载方案的主要是以腾讯系为主,包括微信的Tinker、QQ空间的超级补丁、手机QQ的QFix、饿了么的Amigo和Nuwa等等。

    3.2 底层替换方案

    与类加载方案不同的是,底层替换方案不会再次加载新类,而是直接在Native层修改原有类,由于是在原有类进行修改限制会比较多,不能够增减原有类的方法和字段,如果我们增加了方法数,那么方法索引数也会增加,这样访问方法时会无法通过索引找到正确的方法,同样的字段也是类似的情况。
    底层替换方案和反射的原理有些关联,就拿方法替换来说,方法反射我们可以调用java.lang.Class.getDeclaredMethod,假设我们要反射Key的show方法,会调用如下所示。

       Key.class.getDeclaredMethod("show").invoke(Key.class.newInstance());
    

    Android 8.0的invoke方法,如下所示。
    libcore/ojluni/src/main/java/java/lang/reflect/Method.java

        @FastNative
        public native Object invoke(Object obj, Object... args)
                throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;
    
    

    invoke方法是个native方法,对应Jni层的代码为:
    art/runtime/native/java_lang_reflect_Method.cc

    static jobject Method_invoke(JNIEnv* env, jobject javaMethod, jobject javaReceiver,
                                 jobject javaArgs) {
      ScopedFastNativeObjectAccess soa(env);
      return InvokeMethod(soa, javaMethod, javaReceiver, javaArgs);
    

    Method_invoke函数中又调用了InvokeMethod函数:
    art/runtime/reflection.cc

    jobject InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod,
                         jobject javaReceiver, jobject javaArgs, size_t num_frames) {
    
    ...
      ObjPtr<mirror::Executable> executable = soa.Decode<mirror::Executable>(javaMethod);
      const bool accessible = executable->IsAccessible();
      ArtMethod* m = executable->GetArtMethod();//1
    ...
    }
    

    注释1处获取传入的javaMethod(Key的show方法)在ART虚拟机中对应的一个ArtMethod指针,ArtMethod结构体中包含了Java方法的所有信息,包括执行入口、访问权限、所属类和代码执行地址等等,ArtMethod结构如下所示。
    art/runtime/art_method.h

    class ArtMethod FINAL {
    ...
     protected:
      GcRoot<mirror::Class> declaring_class_;
      std::atomic<std::uint32_t> access_flags_;
      uint32_t dex_code_item_offset_;
      uint32_t dex_method_index_;
      uint16_t method_index_;
      uint16_t hotness_count_;
     struct PtrSizedFields {
        ArtMethod** dex_cache_resolved_methods_;//1
        void* data_;
        void* entry_point_from_quick_compiled_code_;//2
      } ptr_sized_fields_;
    }
    

    ArtMethod结构中比较重要的字段是注释1处的dex_cache_resolved_methods_和注释2处的entry_point_from_quick_compiled_code_,它们是方法的执行入口,当我们调用某一个方法时(比如Key的show方法),就会取得show方法的执行入口,通过执行入口就可以跳过去执行show方法。
    替换ArtMethod结构体中的字段或者替换整个ArtMethod结构体,这就是底层替换方案。
    AndFix采用的是替换ArtMethod结构体中的字段,这样会有兼容问题,因为厂商可能会修改ArtMethod结构体,导致方法替换失败。Sophix采用的是替换整个ArtMethod结构体,这样不会存在兼容问题。
    底层替换方案直接替换了方法,可以立即生效不需要重启。采用底层替换方案主要是阿里系为主,包括AndFix、Dexposed、阿里百川、Sophix。

    3.3 Instant Run方案

    除了资源修复,代码修复同样也可以借鉴Instant Run的原理, 可以说Instant Run的出现推动了热修复框架的发展。
    Instant Run在第一次构建apk时,使用ASM在每一个方法中注入了类似如下的代码:

    IncrementalChange localIncrementalChange = $change;//1
    		if (localIncrementalChange != null) {//2
    			localIncrementalChange.access$dispatch(
    					"onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
    							paramBundle });
    			return;
    		}
    

    其中注释1处是一个成员变量localIncrementalChange ,它的值为$change$change实现了IncrementalChange这个抽象接口。当我们点击InstantRun时,如果方法没有变化则$change为null,就调用return,不做任何处理。如果方法有变化,就生成替换类,这里我们假设MainActivity的onCreate方法做了修改,就会生成替换类MainActivity$override,这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderImpl类,这个类的getPatchedClasses方法会返回被修改的类的列表(里面包含了MainActivity),根据列表会将MainActivity的$change设置为MainActivity$override,因此满足了注释2的条件,会执行MainActivity$overrideaccess$dispatch方法,accessdispatch方法中会根据参数&quot;onCreate.(Landroid/os/Bundle;)V&quot;执行‘MainActivitydispatch方法中会根据参数&quot;onCreate.(Landroid/os/Bundle;)V&quot;执行`MainActivitydispatch"onCreate.(Landroid/os/Bundle;)V"MainActivityoverride`的onCreate方法,从而实现了onCreate方法的修改。
    借鉴Instant Run的原理的热修复框架有Robust和Aceso。


    这里不仅分享大前端、Android、Java等技术,还有程序员成长类文章。
    ZmD7sH.jpg
    展开全文
  • 在上篇Android ClassLoader浅析中我们分析了安卓ClassLoader和热更新原理,这篇我们在上篇热更新分析的基础上写个简单的demo实践一下。 概述 我们先回顾下热更新原理 PathClassLoader是安卓中默认的类加载器,...

    前言

    在上篇Android ClassLoader浅析中我们分析了安卓ClassLoader和热更新的原理,这篇我们在上篇热更新分析的基础上写个简单的demo实践一下。

    概述

    我们先回顾下热更新的原理

    PathClassLoader是安卓中默认的类加载器,加载类是通过findClass()方法,而这个方法最终是通过遍历DexPathList中的Element[]数组加载我们需要的类,那么要想实现热更新只需要在出问题的类还没加载前,把补丁的Element插入到数组前面,这样加载的时候就会优先加载已经修复的类,从而实现了bug的修复。

    原理知道了再来屡一下实现思路。

    1. 通过DexClassLoader加载补丁,然后通过反射拿到生成的Element[]数组
    2. 拿到安卓中默认的类加载器PathClassLoader,然后通过反射拿到Element[]数组
    3. 将补丁Element[]和系统的Element[]数组合并(补丁元素放在合并数组前面),并重新赋值给PathClassLoader

    Show Code

    在showcode之前我们还有个重要的事情要做就是贴出类加载中相关的源码,因为等会反射会用到。DexClassLoaderPathClassLoader只是调用了BaseDexClassLoader构造方法这里就不贴了。

    public class BaseDexClassLoader extends ClassLoader {
        private final DexPathList pathList;
        
        public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                String librarySearchPath, ClassLoader parent, boolean isTrusted) {
            super(parent);
            this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
        }
        
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
            Class c = pathList.findClass(name, suppressedExceptions);
            return c;
        }
    }
    
    final class DexPathList {
    	private Element[] dexElements;
        DexPathList(ClassLoader definingContext, String dexPath,
                String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
            this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                               suppressedExceptions, definingContext, isTrusted);
        }
        
        public Class<?> findClass(String name, List<Throwable> suppressed) {
            for (Element element : dexElements) {
                Class<?> clazz = element.findClass(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
    
            if (dexElementsSuppressedExceptions != null) {
                suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
            }
            return null;
        }
    }
    
    
    

    好了接下来就是热更新的核心代码了

    public class HotFixUtil {
    
        private final String TAG = "zhuliyuan";
        private final String FIELD_DEX_ELEMENTS = "dexElements";
        private final String FIELD_PATH_LIST = "pathList";
        private final String CLASS_NAME = "dalvik.system.BaseDexClassLoader";
    
        private final String DEX_SUFFIX = ".dex";
        private final String JAR_SUFFIX = ".jar";
        private final String APK_SUFFIX = ".apk";
        private final String SOURCE_DIR = "patch";
        private final String OPTIMIZE_DIR = "odex";
    
        public void startFix() throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException {
            // 默认补丁目录  /storage/emulated/0/Android/data/rocketly.hotfixdemo/files/patch
            File sourceFile = MyApplication.getContext().getExternalFilesDir(SOURCE_DIR);
            if (!sourceFile.exists()) {
                Log.i(TAG, "补丁目录不存在");
                return;
            }
            // 默认 dex优化存放目录  /data/data/rocketly.hotfixdemo/app_odex
            File optFile = MyApplication.getContext().getDir(OPTIMIZE_DIR, Context.MODE_PRIVATE);
            if (!optFile.exists()) {
                optFile.mkdir();
            }
            StringBuilder sb = new StringBuilder();
            File[] listFiles = sourceFile.listFiles();
            for (int i = 0; i < listFiles.length; i++) {//遍历查找文件中patch开头, .dex .jar .apk结尾的文件
                File file = listFiles[i];
                if (file.getName().startsWith("patch") && file.getName().endsWith(DEX_SUFFIX)//这里我默认的补丁文件名是patch
                        || file.getName().endsWith(JAR_SUFFIX)
                        || file.getName().endsWith(APK_SUFFIX)) {
                    if (i != 0) {
                        sb.append(File.pathSeparator);//多个dex路径 添加默认分隔符 :
                    }
                    sb.append(file.getAbsolutePath());
                }
            }
            String dexPath = sb.toString();
            String optPath = optFile.getAbsolutePath();
    
            ClassLoader pathClassLoader = MyApplication.getContext().getClassLoader();//拿到系统默认的PathClassLoader加载器
            DexClassLoader dexClassLoader = new DexClassLoader(dexPath, optPath, null, MyApplication.getContext().getClassLoader());//加载我们自己的补丁dex
            Object pathElements = getElements(pathClassLoader);//获取PathClassLoader Element[]
            Object dexElements = getElements(dexClassLoader);//获取DexClassLoader Element[]
            Object combineArray = combineArray(pathElements, dexElements);//合并数组
            setDexElements(pathClassLoader, combineArray);//将合并后Element[]数组设置回PathClassLoader pathList变量
        }
    
        /**
         * 获取Element[]数组
         */
        private Object getElements(ClassLoader classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
            Class<?> BaseDexClassLoaderClazz = Class.forName(CLASS_NAME);//拿到BaseDexClassLoader Class
            Field pathListField = BaseDexClassLoaderClazz.getDeclaredField(FIELD_PATH_LIST);//拿到pathList字段
            pathListField.setAccessible(true);
            Object DexPathList = pathListField.get(classLoader);//拿到DexPathList对象
            Field dexElementsField = DexPathList.getClass().getDeclaredField(FIELD_DEX_ELEMENTS);//拿到dexElements字段
            dexElementsField.setAccessible(true);
            return dexElementsField.get(DexPathList);//拿到Element[]数组
        }
    
        /**
         * 合并Element[]数组 将补丁的放在前面
         */
        private Object combineArray(Object pathElements, Object dexElements) {
            Class<?> componentType = pathElements.getClass().getComponentType();
            int i = Array.getLength(pathElements);
            int j = Array.getLength(dexElements);
            int k = i + j;
            Object result = Array.newInstance(componentType, k);// 创建一个类型为componentType,长度为k的新数组
            System.arraycopy(dexElements, 0, result, 0, j);
            System.arraycopy(pathElements, 0, result, j, i);
            return result;
        }
    
        /**
         * 将Element[]数组 设置回PathClassLoader
         */
        private void setDexElements(ClassLoader classLoader, Object value) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
            Class<?> BaseDexClassLoaderClazz = Class.forName(CLASS_NAME);
            Field pathListField = BaseDexClassLoaderClazz.getDeclaredField(FIELD_PATH_LIST);
            pathListField.setAccessible(true);
            Object dexPathList = pathListField.get(classLoader);
            Field dexElementsField = dexPathList.getClass().getDeclaredField(FIELD_DEX_ELEMENTS);
            dexElementsField.setAccessible(true);
            dexElementsField.set(dexPathList, value);
        }
    }
    
    

    主要就是通过反射获取字段然后数组合并在设置回去,我基本都贴上了注释比较容易看懂就不过多说明了。

    不过有两点需要注意

    1. 我默认是加载名称为patch的文件
    2. 因为有文件读写这里别忘了加上读写权限并且授予权限,我之前在target27上测试的,搞了好久才发现权限没打开。建议target低于23测试,不然demo中没做权限申请得手动授予。
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
        <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    

    这里贴上demo地址HotFixDemo

    测试

    加载补丁

    demo中是在MainActivity中有两个按钮,点击加载补丁按钮默认加载/storage/emulated/0/Android/data/rocketly.hotfixdemo/files/patch目录下的补丁,然后测试按钮是调用Functiontest()方法默认会抛出一个运行时异常。

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            findViewById(R.id.loadPatch).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    try {
                        new HotFixUtil().startFix();//加载补丁
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (NoSuchFieldException e) {
                        e.printStackTrace();
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    }
                }
            });
    
            findViewById(R.id.test).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    new Function().test();//测试
                }
            });
    
        }
    }
    
    public class Function {
    
        public void test() {
            throw new RuntimeException();
            //        Toast.makeText(MyApplication.getContext(),"补丁加载成功",Toast.LENGTH_LONG).show();
        }
    }
    

    那么我们先将这个有bug的apk安装到手机这个时候点击测试是会崩溃的。

    生成class文件

    Functiontest()方法异常代码注释了打开Toast代码注释,点击AS的Rebuild Project

    然后在app的build/intermediates/classes/debug/rocketly/hotfixdemo/ 目录下可以找到编译好的Function.class文件

    生成Dex文件

    接下来将Function.class文件连带包目录复制到一个自己指定的目录,我这里复制到桌面dex文件夹下

    然后通过dx指令生成dex文件

    dx指令的使用跟java指令的使用条件一样,有2种选择:

    1. 配置环境变量(添加到classpath),然后命令行窗口(终端)可以在任意位置使用。
    2. 不配环境变量,直接在build-tools/安卓版本 目录下使用命令行窗口(终端)使用。

    由于这个指令不常使用所以我直接切换到目录下运行命令为:

    dx --dex --output=输出的dex文件完整路径 (空格) 要打包的完整class文件所在目录

    把Dex文件推到SD卡上

    在通过adb命令adb push <local> <remote>将dex文件推到手机指定目录,我demo中是推到/storage/emulated/0/Android/data/rocketly.hotfixdemo/files/patch目录下。

    重启app,点击测试可以发现还是崩溃,然后再次启动app点击加载补丁再点击测试弹出补丁加载成功的toast代表补丁加载成功,这里就大功告成了。

    展开全文
  • Android插件化、热更新原理以及简单实践!
  • android热更新机制

    2018-06-19 09:19:13
    Android 不仅系统版本众多,机型众多,而且各个市场都各有各的政策和审核速度,每次发布一个版本对于开发同学来讲都是一种漫长的煎熬。相比于 iOS 两三天就能达到 80% 的覆盖速度而言,Android 应用版本升级至少需要...
  • Android热更新

    2020-03-30 10:00:31
    二、热更新原理 三、目前市场上热更新框架的对比 四、热更新实践(Sophix) 一、什么是热更新 用来紧急修复线上版本的bug,而且是在用户无感知的情况下,自动修复bug。我们之前的一个开发流程是,开发-测试-发包-...
  • Android热更新简介

    2017-12-19 15:56:42
    如果你交给某人一个程序,你将折磨他一整天;如果你教某人如何编写程序,你将折磨他一辈子。 什么是热更新? 一般我们学习一门新技术,我们都会首先去了解,这门技术是做什么的,有什么用...说到热更新原理,不得不
  • Android热更新方案Robust——美团热更新(热修复)使用介绍
  • Android插件化 最经换了工作,公司的项目比较庞大,很多地方都运用了插件化,插件化说简单就是把部分功能进行打包成专门的apk、dex等文件,当宿主app需要用到此功能的时候才去加载插件;插件不仅可以实现一些功能的...
  • android热修复原理总结

    2016-05-08 01:10:00
    背景当app发布之后如果出现了紧急的线上bug,整个公司都会为此忙的焦头烂额,现公司如果线上出现严重的P1级bug,甚至大半夜整个项目组都得来紧急修复上线,而bug的原因可能仅仅是传错了...原理github上的修复框架如n
  • 2017年6月,阿里巴巴手淘技术团队推出了史上首个非侵入式移动热更新解决方案——Sophix。在Android热修复的三大领域:代码修复、资源修复、SO修复方面,以及方案的安全性和易用性方面,Sophix都做到了业界领先。 ...
  • 声明:本篇文章禁止转载,原创地址:...那就来写写so的修复,其原理和class的修复是一样的,但是so的修复的需求并不高,就当做学习吧。首先来总结一下Android的ClassLoade
  • Android热更新技术的研究与实现之研究篇———概念讲解——–热更新 相关概念这个词出现的时间已经很久了,感觉现在要找工作才来看是晚了不少,但是好东西什么时候学习都不晚的。 今天看到一句话,和大家分享下,...
  • Android热更新方案Robust

    2018-04-09 12:53:46
    美团•大众点评是中国最大的O2O交易平台,目前已拥有近6亿用户,合作各类商户达432万,订单峰值突破1150万单。美团App是平台主要的入口之一,O2O交易场景的复杂性决定了App稳定性要达到近乎苛刻的要求。...
1 2 3 4 5 ... 20
收藏数 13,004
精华内容 5,201
关键字:

android 原理 更新 热