Android热更新

Android热更新

什么是热修复?

热修复提出于2014年,兴起于2016年,尤其是在Instant run 问世以后,各种热修复技术相继涌出。
是一种摆脱传统发版方案直接使用补丁来更新app内容,不需要重新下载安装apk等略过一系列繁琐过程的新兴技术,目前国内部分成熟App都拥有自己的热修复技术,如:手淘、QQ、微信、美团、饿了么等。

热修复有什么优势&为什么要使用热修复?

来看一个场景:公司一个项目A在上线后发现一个严重bug如果不紧急修复可能导致用户流失,这种情况下如果是传统的app更新就很麻烦了大概是这个流程:


这期间重新发版涉及到提交测试环节,这样修复一个bug很不及时,如果使用热修复方案,它将变得很简单:


其优势为:

  • 无需重新发版,简单高效
  • 用户无感知,无需下载新应用,代价小
  • 修复成功率高,挽回用户群体

热修复是如何工作的?

  • 2017年6月手淘联合阿里云正式发布了新一代非侵入式Android热修复方案 - Sophix
    它能修复:代码、资源、SO库,下图为Sophix与微信和饿了么热修复技术对比表

从表中我们能知道个大概,就是Sophix似乎更值得使用一下。
分别介绍QQ空间超级不定、微信Tinker和Sophix的前身HotFix各自的工作原理。

首先了解一下apk的执行过程:代码被编译Build后生成apk文件,其实在里面生成了一个classes.dex文件。

我们解压一个apk文件如下图:



这个classes.dex就是所有代码的集合,是一个可执行文件,android运行apk实质是解压apk运行里面的这个的dex文件。
apk首次运行的时候会对这个dex文件进行优化,优化后生成一个odex文件,存在于缓存中,下次再启动就直接打开这个odex文件,达到快速打开目的。而执行apk的过程就是遍历这个dex并作出相应操作的过程,遍历后的dex方法存放在一个Elements数组中,它的长度限制是65536.即日常说的65K.
如果我们apk因为太庞大或者是引用三方库太多导致方法数超过65K,就会报错.
而谷歌已经在Android 5.0开始支持Multdex.

在知道上面信息后,我们谈谈这三家的热修复是如何实现的
1.QQ空间超级补丁:基于DEX分包方案,使用了多DEX加载的原理,大致的过程就是:把BUG方法修复以后,放到一个单独的DEX里,插入到dexElements数组的最前面,让虚拟机去加载修复完后的方法。


当patch.dex中包含Test.class时就会优先加载,在后续的DEX中遇到Test.class的话就会直接返回而不去加载,这样就达到了修复的目的.

2.微信Tinker:微信针对QQ空间超级补丁技术的不足提出了一个提供DEX差量包,整体替换DEX的方案。主要的原理是与QQ空间超级补丁技术基本相同,区别在于不再将patch.dex增加到elements数组中,而是差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并,然后整体替换掉旧的DEX文件,以达到修复的目的。



  • 组件化—-就是将一个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多。

    增量更新



3.AndFix:不同于QQ空间超级补丁技术和微信Tinker通过增加或替换整个DEX的方案,提供了一种运行时在Native修改Filed指针的方式,实现方法的替换,达到即时生效无需重启,对应用无性能消耗的目的。

AndFix与HotFix的关系如下:


AndFix实现原理:


AndFix实现过程:


更详细的说明戳这里:三大流派之简单对比

阿里云Sophix热修复之简单使用

Sophix集成示例:

第一步:找到Project的build.gradle文件,在allProjects节点下加上如下代码:
repositories {
          maven { url"http://maven.aliyun.com/nexus/content/repositories/releases"}
}
第二步:找到Module的build.gradle文件,添加依赖:

compile'com.aliyun.ams:alicloud-android-hotfix:3.0.7'
然后同步project

第三步:添加权限,SDK使用到以下权限
  <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <!--<! -- 外部存储读权限,调试工具加载本地补丁需要 –>-->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

READ_EXTERNAL_STORAGE/ACCESS_WIFI_STATE 权限属于Dangerous Permissions,自行做好android6.0以上的运行时权限获取

第四步:密钥等配置,在application节点下加入以下配置:
<meta-data
android:name="com.taobao.android.hotfix.IDSECRET"
android:value="App ID" />
<meta-data
android:name="com.taobao.android.hotfix.APPSECRET"
android:value="App Secret" />
<meta-data
android:name="com.taobao.android.hotfix.RSASECRET"
android:value="RSA密钥" />
第五步:登录阿里云热修复管理控制台,填入对应3个value
第六步:代码集成

在Application的attachBaseContext方法里加入Sophix初始化

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        SophixManager.getInstance().setContext(this)
                .setAppVersion(getAppVersion())
                .setAesKey(null)
                .setEnableDebug(true)
                .setPatchLoadStatusStub(new PatchLoadStatusListener() {
                    @Override
                    public void onLoad(final int mode, final int code, final String info, final int handlePatchVersion) {
                        // 补丁加载回调通知
                        if (code == PatchStatus.CODE_LOAD_SUCCESS) {
                            // 表明补丁加载成功
                        } else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
                            // 表明新补丁生效需要重启. 开发者可提示用户或者强制重启;
                            // 建议: 用户可以监听进入后台事件, 然后应用自杀
                        } else if (code == PatchStatus.CODE_LOAD_FAIL) {
                            // 内部引擎异常, 推荐此时清空本地补丁, 防止失败补丁重复加载
                             SophixManager.getInstance().cleanPatches();
                        } else {
                            // 其它错误信息, 查看PatchStatus类说明
                        }
                    }
                }).initialize();
    }
       
    @Override
    public void onCreate() {
        super.onCreate();
        ......
       // queryAndLoadNewPatch不可放在attachBaseContext 中,否则无网络权限,建议放在后面任意时刻,如onCreate中
        SophixManager.getInstance().queryAndLoadNewPatch();
}

自此SDK的集成已经差不多完成,官方给出了很详细的集成方法,官方集成文档

第七步:生成热修复补丁

我们直接看官方文档这里面写的很详细,细到每个设置每个参数都有说明

第八步:调试并发布补丁




  • 组件化—-就是将一个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、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和重新发布。老是发布版本用户会疯掉的!!!(好吧 猿猿们也会疯掉。。)

update again!!! 猿猿-Bug-Fight

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

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

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

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

good job!!! so-cool

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

在我们写好的安卓项目中,有很多逻辑代码,在预编译和编译阶段互相连在一起,各种业务逻辑的链接和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的入口函数来实现。

Dexposed

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

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

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

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

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

  4. 通过在线下载的方式从服务器下载补丁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的实现方式(画的丑勿怪⊙﹏⊙):

AndFix实现过程

  1. 首先添加依赖

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

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

    patchManager = new PatchManager(context);

    patchManager.init(appversion);//current version

    patchManager.loadPatch();

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

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

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

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

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

    AndFix更新的原理:

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

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

    3. 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>();//初始化存在Patch类的集合,此类适合大并发
    mLoaders = new ConcurrentHashMap<String, ClassLoader>();//初始化存放类对应的类加载器集合
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

其中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;
        }
    }
}

。。。。。。。。。。。。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

然后是对版本的初始化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文件加载到内存
    }
}
/*************省略初始化、删除、加载具体方法实现*****************/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

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<String, List<String>>();
        Attributes.Name attrName;
        String name;
        List<String> 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();
        }
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

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

public void loadPatch() {
    mLoaders.put("*", mContext.getClassLoader());// wildcard
    Set<String> patchNames;
    List<String> 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方法
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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

 public synchronized void fix(File file, ClassLoader classLoader,
        List<String> 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<String> 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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99

从上面的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
  • 1
  • 2
  • 3

从源码的注释也能看出来,因为安卓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方法
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

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_);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

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

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

  2. 修改访问权限的属性为public

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

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

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

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

  1. 并不能支持所有的方法修复AndFix修复范围

  2. 不支持YunOS

  3. 无法添加新类和新的字段

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

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



----------------------------------------------------------------------------------------------------------------------------------


环信3.0聊天接入中总结经验和遇到的坑!!!

96 
若无初见 
2018.01.09 18:57*  字数 3807  阅读 126 评论 0

扯淡下:

好久没有更新文章了,连续工作了9个小时,拖着疲惫的身体,对环信接入中遇到的一些坑以及经验做一个总结。

说到第三方你可能会想,那多简单啊?人家做好好的东西接入不就好了吗?比如像glide那样直接就可以使用了。 是的,一开始我也是这么认为的,但是没有一个第三方的东西是完全符合需求的。除非你的项目很赶不需要任何修改。然而任何第三方的东西包括glide4.0、dbflow都会面临着文档的升级以及部分api的修改,况且Android手机更新换代特别快,市场上的Android手机定制的特别多特别杂。我相信没有任何一家公司敢拍着胸脯说自己的东西兼容所有机型。

废话不说,我们进入正题。我们使用easyui 作为接入方式。个人认为这样有个好处:可以更加灵活的修改成我们想要的样式以及更加符合的需求但是文档讲述的东西比较少问题也有不少

总结下几个点:

1下载导入


可以去下载第二个包括一个demo以及easeui.然后我们的项目就可以依赖他的easeui作为库。

注意的是:easeui的module里面是不包含所需的jar包的,不过这些jar可以在demo里面拷贝出来


如果不需要那些推送的可以删除相应的jar包

2初始化


这是官网的初始化的方式,然而不知道为什么对easeui的导入方式并没有做真正的介绍。所以需要修改一下的方式


可以看到初始化由 EMClient变成了EaseUI

3注册环信

注意点:环信注册的时候只看用户名,不管密码是否修改,都默认已经注册,错误码:203.所以在真实开发中一定要结合自己的登录接口做处理

4 Android6.0以上 权限动态申请

注意点:只要你是做开发的就一定记得这个坑。因为聊天中包涵了拍照,录音等都会用到权限。然后环信的api并没有说道这一点,所以在接入的时候一定是会出错的。

5 拍照闪退 

我就纳闷了:没有做任何修改,聊天的页面一开始我也是没有经过重写的。为什么点击原版的拍照图标就给我闪退呢?

注意点:首先根据第四点,权限的申请者是一个坑,另外一个还是Android手机兼容性的问题。   

没办法,谁让我们用的是别人的东西。只能去看源代码了。定位到代码的位置

EaseChatFragment类下的selectPicFromCamera方法中:


打开相册


然而到了Android N (7.0)相关的api的修改了,所以代码要修改成


6 不能发送定位

这个其实不算是环信的坑,因为点击发送位置的时候很明显的提示我们


做过定位的都知道,这个时候我们需要去百度开发者中心注册应用并得到appkey,然后配置到AndroidMenifest.xml文件中

7 查看聊天记录中的图片大图的时候闪退

坑爹又一个闪退,不过Android studio很友好的提示我们相应的Activity没有注册。于是乎我们去查看了demo中的配置文件,把相应的Activity注册了。这还没完,查找的过程中发现其实还有几个相关类没有注册。


所以干脆都注册了,免得以后夜长梦多。

8 添加表情

原版中的表情丑的我力吐槽,不过环信2014年开始做,表情旧点可以理解。不过个人认为还是需要更新下吧?毕竟qq 微信都更新了很多了。

那么怎么在聊天界面中添加一组表情包呢?


官方文档

通过官方文档我们可以看到。输入框及里面的表情等等都是属于 EaseChatInputMenu 而我们可以看到 EasEmojiconMenu正是我们想要的界面。


可以看到 源码中提供了一个添加表情组的方法。我们只需添加一个EaseEmojiconGroupEntity对象就行了。那么这个对象要怎么写呢? 方法还是有的。为什么呢? 很简单:源码中默认不是有表情吗? 我们去easeui的drawable包中就一定能找到相应的图片,然后找到他使用的类不就可以得到官方的表情包类了吗?


果然猜的没错,但是等下,这个东西返回的类型不一样啊。于是乎查看了 EaseEmojicon这个类,果然不出所料和EaseEmojiconGroupEntity一定存在关系。(别问为什么,我也不知道。代码研究多了可能会有种直觉吧。好了不装逼,如果非要说原因:因为他们都是表情包中使用的EaseEmojicon从名字上就 感觉是表情基本类,EaseEmojiconGroupEntity看起来是表情组类。所以他们存在对应关系)而源码中提现了他们确实是对象和集合的关系。

参考下其他代码,经过修改

然后在chatFragment中添加。

重点:如何设置头像和昵称

当你根据环信的官方文档做好接入使之可以正常的聊天,但是这个时候你会发现,聊天界面中并没有正确使用头像和昵称。无论是IOS还是安卓,集成环信SDK遇到的第一个问题,就是如何显示自有用户体系中的昵称和头像。运行环信的demo app,注册用户是直接使用环信ID(username)作为用户名,但是在我们实际应用中,需要将自有用户体系的UserId生成GUID作为环信ID(username)、这时候如果不经过处理,则会显示如下界面:

那么要怎么设置头像和昵称呢? 实际上官方文档也已经给出了方法:

方法一:从APP服务器获取昵称和头像

昵称和头像的获取:当收到一条消息(群消息)时,得到发送者的用户ID,然后查找手机本地数据库是否有此用户ID的昵称和头像,如没有则调用APP服务器接口通过用户ID查询出昵称和头像,然后保存到本地数据库和缓存,下次此用户发来信息即可直接查询缓存或者本地数据库,不需要再次向APP服务器发起请求。

昵称和头像的更新:当点击发送者头像时加载用户详情时从APP服务器查询此用户的具体信息然后更新本地数据库和缓存。当用户自己更新昵称或头像时,也可以发送一条透传消息到其他用户和用户所在的群,来更新该用户的昵称和头像。

方法二:从消息扩展中获取昵称和头像

昵称和头像的获取:把用户基本的昵称和头像的URL放到消息的扩展中,通过消息传递给接收方,当收到一条消息时,则能通过消息的扩展得到发送者的昵称和头像URL,然后保存到本地数据库和缓存。当显示昵称和头像时,请从本地或者缓存中读取,不要直接从消息中把赋值拿给界面(否则当用户昵称改变后,同一个人会显示不同的昵称)。

昵称和头像的更新:当扩展消息中的昵称和头像 URI 与当前本地数据库和缓存中的相应数据不同的时候,需要把新的昵称保存到本地数据库和缓存,并下载新的头像并保存到本地数据库和缓存。

详细文档参考:http://docs.easemob.com/im/490integrationcases/10nickname

鉴于实际情况,我使用第一种方式。

第一种设置方式参考:http://www.imgeek.org/article/825308757



那么好,我们就开始跟他一步一步做:

1、将简版demo里的cache包(5个java文件)复制到自己项目里。

下载环信android简版Demo:

环信Android简版DEMO

昵称头像用到的工具类、model都在这个cache包里。 

类介绍:


拷贝进去,好吧一堆错误。忍了,继续往下。

2.增加第三方依赖库。根目录下的 build.gradle 下:

compile 'com.j256.ormlite:ormlite-android:5.0'

compile 'com.google.code.gson:gson:2.8.0'

ormlite:操作sqlite数据库

gson:json对象转换 

好吧,看到这个我就知道要蛋疼了。数据库使用的是 ormlite 而我使用的是性能相对更好的 dbflow.。不过继续忍了。

3.设置用户信息提供者:

在DemoHelper.java的getUserInfo函数里(第824行)增加如下代码: 

环信头像昵称显示使用的是提供者模式(EaseUserProfileProvider),只要设置了用户信息提供者(setUserProfileProvider),EaseUI界面里显示用户昵称和头像时,就会调用这个getUserInfo函数。

并且注释之前获取昵称头像的方法:

然后,根据不同的方案,开发者可以选择不同步骤:

从开发者自己的APP服务器获取的步骤:

将UserCacheManager.java中第54行-62行代码,换成:通过okhttp(或者retrofit、volley)调用api接口,根据用户环信ID,从开发app服务器获取用户昵称头像。下面两张图是改之前和改之后的效果:


改之前(用第三方云存储):


改之后(用开发者自己服务器):


通过以上的介绍我们可以知道,除了一开始是5个类,最最重要的就是DemoHelper这个类。好吧一起copy进去。

这个时候估计你我都疯了,因为没有一类是对的。都是报红,这个时候怎么办呢?这个时候我们就应该研究源码了。前面我们也说了,第三方的东西需要根据具体的需求做修改。

暂时先不修改那些代码,细心的你也许发现了一句话:环信头像昵称显示使用的是提供者模式(EaseUserProfileProvider),只要设置了用户信息提供者(setUserProfileProvider),EaseUI界面里显示用户昵称和头像时,就会调用这个getUserInfo函数。

重点就在这个EaseUserProfileProvider. 我们将代码定位


上文也说了:环信头像昵称显示使用的是提供者模式EaseUserProfileProvider。所以此时我们将代码定位到getUserProfileProvider的调用者。


咦,我们惊喜的发现了设置头像和昵称的方法,以设置头像为例:


通过getUserInfo的方法获取对象然后设置头像。


所以正如官方文档说的我们只要设置setUserProfileProvider这个方法就可以在聊天的时候设置进去头像和昵称。


所以我们在demo中可以看到DemoHelper设置了setUserProfileProvider这个方法。然后我们继续定位 getUserInfo(username)的方法,一步步进去。(这里我就不一一截图了)

你会发现原来这里做的实际就是缓存用户信息,如果数据不存在或者过期了则就去服务器获取,所以一开始它使用了 ormlite数据库做数据的缓存,用第三方云存储做为服务器。

那么好!知道了他的原理,老子再也不会被牵着鼻子走了。



这五个类,真正使用到的

1 UserCacheInfo类作为数据表类


改为



2 UserCacheManager数据库操作





这里打个岔:从代码量以及真正的功能点上,个人强烈推荐使用dbflow作为数据库框架。关于dbflow的详细介绍与使用请查看本人的另一篇日志。传送门:https://www.jianshu.com/p/431f12648da0

而对于DemoHelper这个类,实际上我可以删除大部分的代码,只要添加一个设置内容提供者的代码就行了



getUserInfo(username)就是返回的我们的缓存数据。

这里提一个严重而又低级的错误。 接口中的方法没人调用,那么这个方法是一定不会走的。因为文档中没有体现,我以为他的某些地方已经设置了,所以就没再去调用。然后一直调试断点结果死活不跑,就自然没有数据了。


针对以上的问题,我暂时使用的是自定义的联系人界面(使用recyclerview作为列表),所以只要在adapter中设置没个username的getUser方法即可



有人这个时候也许会说,说好的调用呢?  别急,都封装到相关的utils类里面。


自此,设置头像和昵称就已经做好了,网上的例子真的太多了,但是你也知道,百度上基本都是抄来抄去,而且好一点的也基本都是来之官方文档的代码。但是我的修改完全是一个精简版,将一些无用的代码完全删除,搞清楚他的原理之后做自己的修改。

总结:

环信的坑还是挺多的,但是功能都基本可以,并且收费标准也不是特别高。我们不能说他不好,这和Android本身的兼容性也有很大关系。如果是ios开发,我相信坑不会这么多。我们归咎于它的文档写得不好,希望有所改进吧。

任何第三方的东西如果想要改成自己想要的东西就一定要研究源码,而不是一味的按部就班,实行拿来主义。实际上,我完全可以不用研究这么多,既然demo中的代码都可以我为什么不能直接copy过来使用呢?这样做问题有二:1无用代码添加一堆会使项目显得庞大冗余,  2不利于自己的学习。




----------------------------------------------------------------------------------------------------------------------------------

用户日常支付的流程:

1.浏览商品

2.把要买的商品加入购物车

3.把商品拿到收银台,收银人员处理商品信息

4.告诉收银员支付方式

5.选择支付方式进行支付

6.处理支付结果(成功、失败、取消)

程序中的支付流程中:

1.浏览商品

2.把要买的商品加入购物车

3.把购物车中的商品信息和用户信息和支付方式等信息发送到自己服务器,服务器处理商品信息生成订单,并返回支付串给客户端

4.客户端拿着支付串”,调用第三方服务(支付宝、微信、银联、paypal)完成支付

5.处理支付结果(成功、失败、取消)

  l  同步返回:支付后通知我们的客户端

  l  异步通知:支付后通知我们的服务端

微信登陆与支付

参考网址:http://www.jianshu.com/p/09a58fe0faef

1、创建应用

2)填写基本的应用信息

3)填写平台信息

最后提交,等待审核。(说是7天,一般1天后就审核通过了)

 开放平台商户申请步骤(APP支付方式)

参考网址:http://kf.qq.com/faq/1612267j2eQ3161226jIVbA3.html

第一步:注册开放平台账号

登录开放平台(open.weixin.qq.com),注册成为微信开放平台开发者。

第二步:认证开放平台并创建APP

开放平台需进行开发者资质认证后才可申请微信支付,认证费:300/次;

提交APP基本信息,通过开放平台应用审核,以获得AppID

第三步:提交资料申请微信支付

登录开放平台,点击【管理中心】,选择需要申请支付功能对应的APP,开始填写资料等待审核,审核时间为1-5个工作日内。

第四步:开户成功,登录商户平台进行验证

资料审核通过后,请登录联系人邮箱查收商户号和密码,并登录商户平台填写财付通备付金打的小额资金数额,完成账户验证。(查看验证方法)

第五步:在线签署协议

本协议为线上电子协议,签署后方可进行交易及资金结算,签署完立即生效。点此提前预览协议内容。

第六步:启动设计和开发

支付接口已获得,可根据开发文档进行开发,也可了解成功案例界面示意及素材。

三,支付流程图及步骤

参考网址:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_3


猜你喜欢

转载自blog.csdn.net/zhangkaiyazky/article/details/79648215