Android——Sophix热修复接入

前言

最近年底项目也没事做了,琢磨着研究一下热修复方案。市面上出现很多热修复方案,大致分为几种,一种是dex插桩的、一种Instant run、还有一种是通过底层做修改。本文主要研究阿里云最新的热修复方案Sophix,Sophix较其他的优劣势如下,对比发现Sophix热修复最大的特点就是即使生效(已有方法修改)和冷启动修复(新增方法和资源修改),并且Sophix已经做到兼容Android P,目前市场上比较成熟的主要是微信Tinker和阿里的Sophix,另一篇Android——Tinker热修复接入

Sophix与其他热修复框架对比.png

创建项目

1.1 注册登录

阿里云官网->登陆账号->产品->企业应用->移动云下面的热修复,切换到当前页面先去注册登录,登录之后,如果是之前没用过,会显示立即开通,之前已经开通的话会直接显示管理控制台如下图所示:

sophix热修复首页.png

1.2 开通产品

如果是第一次使用阿里云产品会先开通使用产品,开通之后如下图所示,点击加号开通产品,产品里面包含一系列,相当于一个产品集合,暂时只使用热修复,我开通的如下:

阿里云项目合集.png
1.3 创建热修复项目

点击下图的加号即可创建项目,创建总分为三步,具体如下图:


创建测试sophix热修复项目.png
测试项目配置.png

第二步会产生一个json文件,是当前项目所有阿里云产品的配置,如果只做热修复,只需要关心热修复的三个主要参数,如下图所示的参数(appKey、hotfix.idSecret、hotfix.rsaSecret)。下面是配置信息,如果按照正常接入步骤可以按照文档接入,本文主要采用兼容Android 9.0稳健方式接入,下面的json配置文件尽量不要按照普通接入方式复制在项目下(除非是测试项目)。

"config": {
    "emas.appKey":"27478432",
    "emas.appSecret":"5e2f53a5aa6b8a48e74ac7146688b65c",
    "emas.packageName":"com.example.testsophix",
    "hotfix.idSecret":"2547832-1",
    "hotfix.rsaSecret":"//////",
    "httpdns.accountId":"102949",
    "httpdns.secretKey":"826b1d8ad5119dd34f1a855eedd3cea0"

说明:

  • 应用名称可以随意填写,建议填写项目中的appname,如果以后项目多避免混乱
  • 包名一定要填写清单列表中包名
  • 建议json配置不要放在项目中,如果忘记下载json配置也没关系,创建之后如下图所示点击下载配置即可获取配置文件


    下载配置.png

项目接入SDK

本文主要接入Sophix稳健方式的,兼容Android9.0,因为Android9.0对于部分热修复方式做简单修改,如果普通接入会出现奔溃或者修复不成功问题,下面是具体接入Android studio(3.0)方式,更多方式参考文档

2.1 Project的build.gradle配置

Project项目下的build.gradle文件,添加maven仓库地址,添加如下配置:

repositories {
       maven {
           url "http://maven.aliyun.com/nexus/content/repositories/releases"
       }
    }
2.2 module级别build.gradle配置

module下build.gradle文件添加版本依赖:

 compile 'com.aliyun.ams:alicloud-android-hotfix:3.2.7'
2.3 权限配置
<! -- 网络权限 -->
    <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"/>

说明:

  • 当使用调试工具(后面会提)调试补丁的时候,如果补丁放在本地,需要加读权限,并且Android 6.0以上的读写权限动态申请
  • 联网权限是用于拉取阿里服务器补丁
2.4 配置SDK参数

配置参数有两种方式,一种是清单列表中,第二种是在项目的Application中通过参数的形式,推荐使用第二种(相对较为安全)

2.4.1 AndroidManifest文件配置SDK参数

其中三个参数对应上文提到的json文件里面的三个参数

 <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密钥" />
2.4.2 代码中配置SDK参数
SophixManager.getInstance()
                .setContext(this)
                .setAppVersion(appVersion)
                .setSecretMetaData(null, null, null)//这里填写参数
                .setEnableDebug(true)
                .setEnableFullLog()
                .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) {
                            Log.i(TAG, "sophix load patch success!");
                        } else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
                            // 如果需要在后台重启,建议此处用SharePreference保存状态。
                            Log.i(TAG, "sophix preload patch success. restart app to make effect.");
                        }
                    }
                }).initialize();
  • Sophix初始化的时候.setSecretMetaData(null, null, null)设置三个参数,顺序依次跟AndroidManifest一致。
2.5 混淆配置
#基线包使用,生成mapping.txt
-printmapping mapping.txt
#生成的mapping.txt在app/build/outputs/mapping/release路径下,移动到/app路径下
#修复后的项目使用,保证混淆结果一致
#-applymapping mapping.txt
#hotfix
-keep class com.taobao.sophix.**{*;}
-keep class com.ta.utdid2.device.**{*;}
-dontwarn com.alibaba.sdk.android.utils.**
#防止inline
-dontoptimize
#看情况是否要混淆
-keepclassmembers class com.example.testsophix.MyRealApplication {
        public <init>();
    }

说明:如果项目混淆开启的情况下,在app/build/outputs/mapping/release路径下会生成mapping.txt,建议开启混淆(保证一些参数被混淆)。

2.6 SDK初始化

此稳健方式接入,使用了最新版本的Sophix,最新版本的Sophix采用了代理Application方式接入SDK,使自身的Application和Sophix初始化解耦合。初始化在继承SophixApplication的SophixStubApplication(名称可以自定义)中实现,其中自身的Application不需要做任何修改操作。

2.6.1 Application中初始化Sophix
public class SophixStubApplication extends SophixApplication {

    private final String TAG = "SophixStubApplication";

    private final String hotfixId = "25478432-1";
    private final String hotfixappKey = "5e2f53a5aa6b8a48e74ac7146688b65c";

    /**
     * 此处SophixEntry应指定真正的Application,
     * 并且保证RealApplicationStub类名不被混淆。
     *
     * @keep 注解已经做好了,无需在混淆文件做处理
     */
    @Keep
    @SophixEntry(MyRealApplication.class)
    static class RealApplicationStub {
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //如果需要使用MultiDex,需要在此处调用。
        // MultiDex.install(this);
        initSophix();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        //联网下载新的插件
        SophixManager.getInstance().queryAndLoadNewPatch();
    }

    /**
     * initialize最好放在attachBaseContext最前面
     */
    private void initSophix() {
        SophixManager.getInstance().setContext(this)
                .setAppVersion(BuildConfig.VERSION_NAME)
                .setAesKey(null)
                .setSecretMetaData(hotfixId,hotfixappKey,getString(R.string.hotfixrsaSecret))
                .setEnableDebug(true)
                .setPatchLoadStatusStub(new PatchLoadStatusListener() {
                    @Override
                    public void onLoad(final int mode, final int code, final String info, final int handlePatchVersion) {

                        Log.e(TAG, "修复模式:" + mode);
                        Log.e(TAG, "修复回调code:" + code);
                        Log.e(TAG, "修复信息:" + info);
                        Log.e(TAG, "修复版本:" + handlePatchVersion);

                        // 补丁加载回调通知
                        if (code == PatchStatus.CODE_LOAD_SUCCESS) {
                            // 表明补丁加载成功
                            Log.e(TAG, "表明补丁加载成功");
                        } else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
                            // 表明新补丁生效需要重启. 开发者可提示用户或者强制重启;
                            // 建议: 用户可以监听进入后台事件, 然后应用自杀
                            Log.e(TAG, "表明新补丁生效需要重启. 开发者可提示用户或者强制重启");
                        } else if (code == PatchStatus.CODE_LOAD_FAIL) {
                            // 内部引擎异常, 推荐此时清空本地补丁, 防止失败补丁重复加载
                            // SophixManager.getInstance().cleanPatches();
                            Log.e(TAG, "内部引擎异常, 推荐此时清空本地补丁, 防止失败补丁重复加载");
                        } else {
                            // 其它错误信息, 查看PatchStatus类说明
                            Log.e(TAG, "其它错误信息, 查看PatchStatus类说明");
                        }
                    }
                }).initialize();
    }
}
2.6.2 修改AndroidManifest里面的Application名称
 <application
            android:name="com.my.pkg.SophixStubApplication"
            ... ...>
            ... ...

说明:

  • 需要把AndroidManifest里面的Application名称改为这个新增的SophixStubApplication类(一定要修改,否则无法初始化Sophix)
  • SophixStubApplication必须要继承SophixApplication
  • @SophixEntry(MyRealApplication.class)处的MyRealApplication一定要填写自己本身的Application
  • 这里的@Keep是android.support包中的类,目的是为了防止这个内部静态类的类名被混淆,因为sophix内部会反射获取这个类的,如果项目中没有依赖android.support的话,就需要在progurad里面手动指定RealApplicationStub不被混淆(就是自己的Application)。(详情参见文档)例子如下:
-keepclassmembers class com.my.pkg.MyRealApplication {
        public <init>();
    }
    # 如果不使用android.support.annotation.Keep则需加上此行
    # -keep class com.my.pkg.SophixStubApplication$RealApplicationStub
2.7 接口说明
2.7.1 initialize方法
  • initialize(): <必选>
    该方法主要做些必要的初始化工作以及如果本地有补丁的话会加载补丁, 但不会自动请求补丁。因此需要自行调用queryAndLoadNewPatch方法拉取补丁。这个方法调用需要尽可能的早, 必须在Application的attachBaseContext方法的最前面调用(在super.attachBaseContext之后,如果有Multidex,也需要在Multidex.install之后), initialize()方法调用之前你需要先调用如下几个方法进行一些必要的参数设置, 方法调用说明如下:
  • setContext(application): <必选> 传入入口Application即可
  • setAppVersion(appVersion): <必选> 应用的版本号
    说明:appVersion在产生补丁的两包必须相同,否则无法加载补丁
    appVersion默认取的module的build.gradle里面的versionName,当然可以自定义,推荐使用versionName,并且最好与热修复控制台的版本保持一致,防止版本多了混乱。
  • setSecretMetaData(idSecret, appSecret, rsaSecret): <可选,推荐使用> 三个Secret分别对应AndroidManifest里面的三个,可以不在AndroidManifest设置而是用此函数来设置Secret。放到代码里面进行设置可以自定义混淆代码,更加安全,此函数的设置会覆盖AndroidManifest里面的设置,如果对应的值设为null,默认会在使用AndroidManifest里面的。
  • setAesKey(aesKey): <可选> 用户自定义aes秘钥, 会对补丁包采用对称加密。这个参数值必须是16位数字或字母的组合,是和补丁工具设置里面AES Key保持完全一致, 补丁才能正确被解密进而加载。此时平台无感知这个秘钥, 所以不用担心阿里云移动平台会利用你们的补丁做一些非法的事情。
  • setPatchLoadStatusStub(new PatchLoadStatusListener()): <可选,推荐> 设置patch加载状态监听器, 该方法参数需要实现PatchLoadStatusListener接口, 接口参数说明见2.7.2说明
2.7.2 PatchLoadStatusListener接口

该接口需要自行实现并传入initialize方法中, 补丁加载状态会回调给该接口, 参数说明如下:

  • mode: 无实际意义, 为了兼容老版本, 默认始终为0
  • code: 补丁加载状态码(code=1代表加载成功), 更多文档(超链接)PatchStatus类说明
  • info: 补丁加载详细说明
  • handlePatchVersion: 当前处理的补丁版本号, 0:无 -1:本地补丁 其它:后台补丁
2.7.3 queryAndLoadNewPatch方法

该方法主要用于查询服务器是否有新的可用补丁. SDK内部限制连续两次queryAndLoadNewPatch()方法调用不能短于3s, 否则的话就会报code:19的错误码. 如果查询到可用的话, 首先下载补丁到本地。

  • 应用原本没有补丁, 那么如果当前应用的补丁是热补丁, 那么会立刻加载(不管是冷补丁还是热补丁). 如果当前应用的补丁是冷补丁, 那么需要重启生效.

  • 应用已经存在一个补丁, 请求发现有新补丁后,本次不受影响。并且在下次启动时补丁文件删除, 下载并预加载新补丁。在下下次启动时应用新补丁。

  • 补丁在后台发布之后, 并不会主动下行推送到客户端, 需要手动调用queryAndLoadNewPatch方法查询后台补丁是否可用.
    只会下载补丁版本号比当前应用存在的补丁版本号高的补丁, 比如当前应用已经下载了补丁版本号为5的补丁, 那么只有后台发布的补丁版本号>5才会重新下载.

同时1.4.0以上版本服务后台上线了“一键清除”补丁的功能, 所以如果后台点击了“一键清除”那么这个方法将会返回code:18的状态码. 此时本地补丁将会被强制清除, 同时不清除本地补丁版本号

2.7.4 killProcessSafely方法

可以在PatchLoadStatusListener监听到CODE_LOAD_RELAUNCH后在合适的时机,调用此方法杀死进程。注意,不可以直接Process.killProcess(Process.myPid())来杀进程,这样会扰乱Sophix的内部状态。因此如果需要杀死进程,建议使用这个方法,它在内部做一些适当处理后才杀死本进程。

2.7.5 cleanPatches()方法

清空本地补丁,并且不再拉取被清空的版本的补丁。正常情况下不需要开发者自己调用,因为Sophix内部会判断对补丁引发崩溃的情况进行自动清空。

补丁管理

简单说一下Sophix修复的过程,首先需要两个apk包,一个是线上或者测试bug包,另一个是修复好问题的apk包,通过使用补丁工具,两个不同apk包会产生一个补丁,补丁上传阿里服务器,扫码下载补丁或者手动存放到本地,千万不要一开始就直接发布补丁,首先要用调试工具调试一下,查看是否完全符合自己的要求,当补丁符合要求再去发布,具体发布详情见下面的发布。并且要保证两个包是同一个版本,一个版本同时只能存在一个补丁,所以如果当前最近的补丁不是第一个补丁,那么最新的补丁一定是结合修复了之前所有补丁之后通过补丁工具生成的补丁。

3.1 创建补丁
3.1.1 下载补丁和调试工具

patch补丁包生成需要使用到打补丁工具SophixPatchTool, 如还未下载打包工具,请前往下载Android打包工具。

该工具提供了Windows和macOS和Linux版本,Windows下运行SophixPatchTool.exe,macOS下运行SophixPatchTool.app,Linux下(Ubuntu 16.04 64bit最佳)运行SophixPatchTool。并且需要安装Java环境且在JDK7或以上才能正常使用。
说明:一些注意事项在补丁工具中会介绍(两张图片介绍)

3.1.2 补丁工具使用

解压补丁工具,运行SophixPatchTool.exe,如下图:


补丁调试工具主界面.png
补丁签名.jpg

说明:

  • 签名是必须要填写的,高级设置里面一些参数,具体情况而定
  • 产生的补丁一定不要修改名称(补丁名称:sophix-patch.jar)
3.2 上传补丁

进入阿里云控制台,点击右侧的移动热修复,进入项目补丁管理


补丁管理.png

如上图在管理控制台,找到质量管理下面的移动热修复进入到补丁管理页面

3.2.1 添加补丁版本

此处添加的版本尽量与项目中的versionname保持一致


创建版本.png
3.2.2 上传补丁

上传补丁里面描述,尽量要描述当前补丁修复那些内容,方便以后版本的回滚


上传补丁.jpg
3.2.3 发布调试补丁

点击发布之后,需要我们进行调试发布补丁,接下来进入调试、发布补丁阶段


调试发布.png

调试补丁

4.1 调试工具使用
sophix调试工具

使用说明:
1、使用工具调试之前一定要先安装有bug的版本
2 、接下来连接应用
3、加载补丁有两种方式,一种是扫码二维码的方式,第二种是加载补丁放在本地,直接加载(本地加载需要读权限,涉及到Android6.0动态权限的设置),推荐使用扫描方式加载补丁。
4、通过下方的日志获取当前补丁的加载过程

4.2 加载补丁
扫描补丁.png

扫描补丁之后查看调试工具日志输出,查看补丁情况,如果之前打补丁的时候高级设置里面设置强制冷启动,一定要先杀死当前测试热修复应用的进程,然后再次打开查看打补丁之后的情况。下面列举日志code码经常出现的几种情况更多code值请查看接口文档(超链接):

4.3 输出补丁日志
调试结果.jpg
    int CODE_LOAD_SUCCESS = 1;//加载阶段, 成功
    int CODE_ERR_INBLACKLIST = 4;//加载阶段, 失败设备不支持
    int CODE_REQ_NOUPDATE = 6;//查询阶段, 没有发布新补丁
    int CODE_REQ_NOTNEWEST = 7;//查询阶段, 补丁不是最新的 
    int CODE_DOWNLOAD_SUCCESS = 9;//查询阶段, 补丁下载成功
    int CODE_DOWNLOAD_BROKEN = 10;//查询阶段, 补丁文件损坏下载失败
    int CODE_UNZIP_FAIL = 11;//查询阶段, 补丁解密失败
    int CODE_LOAD_RELAUNCH = 12;//预加载阶段, 需要重启
    int CODE_REQ_APPIDERR = 15;//查询阶段, appid异常
    int CODE_REQ_SIGNERR = 16;//查询阶段, 签名异常
    int CODE_REQ_UNAVAIABLE = 17;//查询阶段, 系统无效
    int CODE_REQ_SYSTEMERR = 22;//查询阶段, 系统异常
    int CODE_REQ_CLEARPATCH = 18;//查询阶段, 一键清除补丁
    int CODE_PATCH_INVAILD = 20;//加载阶段, 补丁格式非法
4.4 补丁说明

说明一:patch是针对客户端具体某个版本的,patch和具体版本绑定

  • 应用当前版本号是1.1.0, 那么只能在后台查询到1.1.0版本对应发布的补丁, 而查询不到之前1.0.0旧版本发布的补丁.

说明二:针对某个具体版本发布的新补丁, 必须包含所有的bugfix, 而不能依赖补丁递增修复的方式, 因为应用仅可能加载一个补丁

  • 针对1.0.0版本在后台发布了一个补丁版本号为1的补丁修复了bug1, 然后发现此时针对这个版本补丁1修复的不完全, 代码还有bug2, 在后台重新发布一个补丁版本号为2的补丁, 那么此时补丁2就必须同时包含bug1和bug2的修复才行, 而不是只包含bug2的修复(bug1就没被修复了)

发布补丁

发布前请严格按照:扫码内测 => 灰度发布 => 全量发布的流程进行,以保证补丁包能够正常在所有Android版本的机型上生效。为了保险起见,理论上应该对每个版本的android手机都测一遍是否生效会比较好。不过,其实只需测试通过以下具有代表性的Android版本就基本没什么大问题了:4.0、4.4、5.1、7.0

注意事项

具体接入细节请参考官方文档:
非稳健方式接入
稳健方式接入

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容