Android 热修复Tinker 在项目中的使用

时间:2022-12-22 05:32:19

热修复技术出来也已经有好长一段时间了,目前比较主流的热修复方案主要有一下几种:

QQ团队基于android dex分包方案提出的热修复方案,代表:Nuwa , Hotfix
Alibaba 提出的热修复方案,代表:AndFix(目前使用最多,兼容问题较严重)
Tecent 提出的热修复方案 代表: tinker (目前性能最优,兼容最好)

blog 上很多大神都对热修复技术做出过自己的分析,我了解的hongyang大神就写过这方面技术分析。链接如下:

http://blog.csdn.net/lmj623565791/article/details/49883661 QQzone分析
http://blog.csdn.net/lmj623565791/article/details/54882693 Tinker 分析

Tinker的实现思想和QQzone类似,本文的重点不在是原理分析上,主要侧重在项目引用上面,顺便带上对Tinker源码的分析,如果你对热修复还不了解的话,强烈建议先去看一下上述推荐的blog。

本篇博客篇幅较长,文末连接进行下载资源。

Tinker项目实战运用

1.Tinker 集成

Tinker 为我们提供了两种方式去集成,一种是命令行接入另外一种是Gradle接入。个人使用的后者,主要是能自动化只需在Terminal执行一下task任务自动编译好补丁包多好啊。第一种接入方式参照上面hongyang 大神的博客,在这主要说一下Gradle接入:github tinker的示例tinker-sample-android 就是采用gradle 接入的。

https://github.com/Tencent/tinker

你可以将tinker-sample-android 中build.gradle 里面的信息都相应摘到自己项目中,注意是。以下是一些注意点:

dependencies 依赖的TINKER_VERSION 在项目根目录下gradle.properties下声明着,同时别忘了在根目录下的build.gradle dependencies 添加上 classpath “com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}”

//通过获取git的版本号来获取TinkerId
def gitSha() {
    try {
        String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
        //...省略
    } catch (Exception e) {
        throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
    }
}

这里如果你们公司的项目不是使用git管理的,那么Tinker 在编译生成TinkerId 必然会报错 tinker id is not set !!!。修改为 String gitRev = tinker_id_6235657

项目中的application 将不在是继承Application,按照SampleApplicationLike中的写,采用编译时注解动态生成application。

@SuppressWarnings("unused")
@DefaultLifeCycle(application = "tinker.sample.android.app.SampleApplication",
                  flags = ShareConstants.TINKER_ENABLE_ALL,
                  loadVerifyFlag = false)
public class SampleApplicationLike extends DefaultApplicationLike {
    private static final String TAG = "Tinker.SampleApplicationLike";

    public SampleApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,long applicationStartElapsedTime,long applicationStartMillisTime,Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }

    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        //you must install multiDex whatever tinker is installed!
        MultiDex.install(base);
        //... 省略
        TinkerManager.installTinker(this);
    }

     @Override
    public void onCreate() {
        super.onCreate();
        //初始化操作 here 
    }

}

其中DefaultLifeCycle 更改成自己项目的application的路径,经过编译之后会生成SampleApplication命名的applicaiton ,在AndroidMainfest.xml中application name 更改为 android:name=”.app.SampleApplication” flags = Tinker_enable_all,Tinker 默认支持 class library resource 三种修复,所以在没有特殊的情况下就选择enable_all 吧 ! loadVerifyFlags 选择 false 无需修改。至于其他所需要的类都复制到你项目中即可。

加载差异包api

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),savePath);

2.接口说明

完整成型的app总是少不了更新模块,大部分会把更新请求操作放到MainActivity中,启动app时请求一次接口根据版本号判断是否需要下载app。那么可以在app更新接口中追加上补丁更新相关字段信息。下面这个json信息是我用Tomcat自己写的一个小的web项目中更新接口http://localhost:8080/MySpringWeb/mvc/getVersion返回的。(web写的很烂,在这我只是简单的说明一下需要什么样的字段,后面我会把源发放出,大家测试的时候就用自己公司的服务器接口吧!)

{"app_name":"app-debug-0414-14-28-13.apk",  *(new app 名字)*
"app_url":"/MySpringWeb/mvc/getApp",  *(app 下载地址)*
"version_type":"3",  *(1.建议更新 2.强制更新 3 不更新)*
"version_code":"1.0", *(app 版本号)*
"remark":"这次我们修复了一些既有的bug,同时增添了一些新的功能.....",  *(app 更新提示)*
"patch_name":"patch_signed_7zip.apk",*(Patch 补丁包名字)*
"patch_url":"/MySpringWeb/mvc/getDex", *(Patch 下载地址)*
"version_patch":"3.0" *(Patch 版本号)*
}
3.Patch 更新类 VersionUpdateManager
/** * created by millerJK on time : 2017/4/14 * description : app版本的更新会使用dialog 提示,差异包更新则是后台自动下载,无需使用到dialog */
public class VersionUpdateManager {

    private static final String TAG = "VersionUpdateManager";

    private static final String SAVE_DIR = "hotfix";

    private static final String PATCH_VERSION_CODE = "version_patch";

    public static final int MESSAGE_UPDATE = 1;

    public static final int MESSAGE_APP_OVER = 2;

    public static final int MESSAGE_PATCH_OVER = 3;

    private String PATCH_NAME;

    private  String APP_NAME;

    private Context mContext;

    private VersionEntity mVersionInfo;

    private String mRootDir;

    private String mSaveApkDirPath;

    private String mSavePatchDirPath;

    private static VersionUpdateManager mVersionUpdateManager;

    private SharedPreferences sp;

    private boolean isCancel = false;

    private boolean needAppUpdate = false;

    private AlertDialog.Builder mBuilder;

    private Dialog mVersionUpdateDialog;

    private ProgressDialog mProgressDialog;

    private Handler mHandler;

    public static VersionUpdateManager getInstance(Context context, VersionEntity mVersionInfo, Handler handler) {
        if (mVersionUpdateManager == null) {
            mVersionUpdateManager = new VersionUpdateManager(context, mVersionInfo, handler);
        }
        return mVersionUpdateManager;
    }

    private VersionUpdateManager(Context context, VersionEntity mVersionInfo, Handler handler) {
        this.mContext = context;
        this.mVersionInfo = mVersionInfo;
        this.mHandler = handler;
        init();
    }

    private void init() {
        sp = context.getSharedPreferences(PATCH_VERSION_CODE, Context.MODE_PRIVATE);
        APP_NAME = mVersionInfo.app_name;
        PATCH_NAME = mVersionInfo.patch_name;
        createFileSavePath();
    }

    private void createFileSavePath()
    {
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
        {
            mRootDir = Environment.getExternalStorageDirectory().getAbsolutePath();
            mRootDir = mRootDir + File.separator + SAVE_DIR;
            mSaveApkDirPath = mRootDir + File.separator + "apk";
            FileUtil.createFolder(mSaveApkDirPath);
            mSavePatchDirPath = mRootDir + File.separator + "patch";
            FileUtil.createFolder(mSavePatchDirPath);
        } else
            Log.e(TAG, "sd is not found");

    }

    public void startTask() {

        Log.e(TAG, "start task");

        if (mVersionInfo == null)
        {
            return;
        }

        //只有在app不需要进行版本升级的时候才会检查补丁版本
        if (needAppUpdate = needAppUpdate(mVersionInfo))
        {
            //showVersionUpdateDialog(); app 跟新dialog弹出 此处省略此操作和Patch 无关,下载文末完整代码查看
        } else if (needPatchUpdate(mVersionInfo)) {
            // TODO: 2017/2/15 patch download
            Log.e(TAG, "******** patch need updates required ********");
            new Thread(downApkRunnable).start();
        }
    }


    /** * whether a app version upgrade is required * * @param entity * @return */
    private boolean needAppUpdate(VersionEntity entity)
    {
       //调用 compareVersion(entity.version_app, info.versionName) 判断是否需要版本更新,此处省略代码
    }

    /** * 设置差异包版本号 * * @param patchVersionCode */
    public void setPatchVersionCode(String patchVersionCode) {
        sp.edit().putString(PATCH_VERSION_CODE, patchVersionCode).commit();
    }

    public void clearPatch(){
        Tinker.with(context.getApplicationContext()).cleanPatch();
    }

    /** * 获取差异包版本号 * * @return */
    public String getPatchVersionCode() {
        String patchVersionCode = sp.getString(PATCH_VERSION_CODE, "0.0");
        return patchVersionCode;
    }

    /** * whether a patch version upgrade is required * * @param entity * @return */
    private boolean needPatchUpdate(VersionEntity entity) {

        String oldPatchVersion = getPatchVersionCode();
        Log.e(TAG, "oldPatchVersion from local :" + oldPatchVersion);
        if (entity == null
                || TextUtils.isEmpty(entity.version_patch)
                || TextUtils.isEmpty(oldPatchVersion))
            return false;

        if (compareVersion(oldPatchVersion, entity.version_patch) >= 0) {
            Log.e(TAG, "******** No patch updates required ********");
            return false;
        } else {
            Log.e(TAG, "******** patch updates required ********");
            return true;
        }

    }

    String savePath;

    /** * patch and apk version update */
    private Runnable downApkRunnable = new Runnable() {
        @Override
        public void run() {

            String fileUrl;

            if (needAppUpdate) {
                fileUrl = mVersionInfo.app_url;
                savePath = mSaveApkDirPath + File.separator + APP_NAME;
                Log.e(TAG, "start downloading APK " + mVersionInfo.app_url);
            } else {
                fileUrl = mVersionInfo.patch_url;
                savePath = mSavePatchDirPath + File.separator + PATCH_NAME;
                Log.e(TAG, "start downloading Patch " + mVersionInfo.patch_url);
            }

            Log.e(TAG, savePath);

            try {
                URL url = new URL(fileUrl);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.connect();
                conn.setConnectTimeout(5000);
                conn.setReadTimeout(5000);
                int length = conn.getContentLength();
                InputStream is = conn.getInputStream();

                File ApkFile = new File(savePath);
                FileOutputStream fos = new FileOutputStream(ApkFile);
                int count = 0;
                byte[] buf = new byte[1024 * 5];

                do {
                    int numread = is.read(buf);
                    count += numread;
                    int progress = (int) (((float) count / length) * 100);
                    sendProgressMessage(progress);
                    Log.e(TAG, "downloading ..." + count + "/" + length + " " + progress + "%");
                    if (numread <= 0) {
                        if (needAppUpdate)
                        {
                            mProgressDialog.dismiss();
                            mHandler.sendEmptyMessage(MESSAGE_APP_OVER);
                            //reset sharePreference patch version code
                            Log.e(TAG, "App download finished !!!!");
                        } else
                        {
                            Message message = mHandler.obtainMessage();
                            message.what = MESSAGE_PATCH_OVER;
                            mHandler.sendMessage(message);
                            //reset sharePreference patch version code
                            Log.e(TAG, "Patch download finished !!!!");
                        }
                        break;
                    }
                    fos.write(buf, 0, numread);
                    fos.flush();
                } while (!isCancel);
                fos.close();
                is.close();
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    };

    private void sendProgressMessage(int progress) {
        Message message = mHandler.obtainMessage();
        message.what = MESSAGE_UPDATE;
        message.obj = progress;
        mHandler.sendMessage(message);
    }

    private void showVersionUpdateDialog(

            DialogInterface.OnClickListener mPositionListener,
            DialogInterface.OnClickListener mNegativeListener) {
           //...根据version_type 弹出相应dialog(强制下载dialog 或者建议下载dialog) 省略代码和patch 更新无关,完整代码点击文末连接下载
    }

    private void showProgressDialog(
            DialogInterface.OnClickListener mNegativeListener) {
            //...省略代码和patch 更新无关,完整代码点击文末连接下载
    }

    public void setProgress(int progress) {
        mProgressDialog.setProgress(progress);
    }

    /** * app 安装 */
    public void startInstall() {
        installApk(mSaveApkDirPath + File.separator + APP_NAME);
    }

    /** * Patch 安装 */
    public void upgradePatch(){
        TinkerInstaller.onReceiveUpgradePatch(context.getApplicationContext(),savePath);
        Log.e(TAG, "newPatchVersion to local:" + mVersionInfo.version_patch);
        setPatchVersionCode(mVersionInfo.version_patch);
    }

    private void installApk(String saveFileName) {

        setPatchVersionCode("0.0");
        clearPatch();

        File apkfile = new File(saveFileName);
        if (!apkfile.exists()) {
            return;
        }
        try {
            unInstall();
            install(apkfile);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /** * uninstall the original application first */
    private void unInstall() {
        Uri uri = Uri.parse("package:" + mContext.getPackageName());
        Intent deleteIntent = new Intent();
        deleteIntent.setType(Intent.ACTION_DELETE);
        deleteIntent.setData(uri);
        mContext.startActivity(deleteIntent);
    }

    /** * install the new application second */
    private void install(File apkfile) {
        Intent i = new Intent(Intent.ACTION_VIEW);
        i.setDataAndType(Uri.parse("file://" + apkfile.toString()),
                "application/vnd.android.package-archive");
        i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        mContext.startActivity(i);

    }

    public int compareVersion(String remoteVersion, String localVersion) {
        //...省略部分代码
        return diff;
    }
}

public class VersionEntity {

    public String app_url; //app 下载地址
    public String patch_url; // 补丁包下载地址
    public String version_app; //开发最新app版本号
    public String version_patch; //最新补丁包版本号
    public String remark;  //更新提示内容
    public String version_type; //1.更新 2.强制更新 3 不更新
    public String app_name;
    public String patch_name;

    public VersionEntity() {
    }

    public VersionEntity(String app_url, String patch_url, String version_app, String version_patch
            , String remark, String version_type, String app_name, String patch_name) {
        this.app_url = app_url;
        this.patch_url = patch_url;
        this.version_app = version_app;
        this.version_patch = version_patch;
        this.remark = remark;
        this.version_type = version_type;
        this.app_name = app_name;
        this.patch_name = patch_name;
    }

 }

代码有些长,但是比较简单,程序入口为startTask(),这方法里面有写一句话 “只有在app 不需要版本升级的时候才会检测补丁是否需更新。”其实想想就知道为什么,每次版本升级必定是老版本bug都修复了,同时有可能添加了一些新功能,我们可以认为最新的app是没有bug的,所以根本就不需要进行补丁检测判断。通过needAppUpdate() 判断app 版本是否需要进行版本更新,needPatchUpdate() 判断补丁是否需要进行升级,其中app版本升级那个分支就不看了不是重点。着重看一下补丁升级分支。

通过sharedPerence 获取保存在本地的patch版本号(初始version = 0)和 VersionEntity中version_patch做比对判断,返回true则开启线程执行下载 runnable ,看一下202-227行,会发送三种Message 给主线程 1. Progress 更新进度 2.APP 下载完毕 3. Patch 下载完毕, 如果App下载完毕则需要调用 installApk方法,首先会执行 setPatchVersionCode(“0.0”); 方法将本地patchVersion 重置,然后执行clearPatch();将 /data/data/com.xxx.xxxx/tinker 删除掉。 如果是 Patch下载完毕则需要调用upgradePatch()方法,同时更新本地Patch 版本保存到SharePerference中。

5.测试

MainActivity 代码:


public class MainActivity extends AppCompatActivity {

    private static final String TAG = "Tinker.MainActivity";

    public static final String BASE_URL = "http://172.27.35.1:8080";//change you ip here

    private VersionUpdateManager mVersionUpdateManager;

    private VersionEntity mEntity;

    private Button mButton;

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case VersionUpdateManager.MESSAGE_APP_OVER:
                    mVersionUpdateManager.startInstall();
                    break;
                case VersionUpdateManager.MESSAGE_PATCH_OVER:
                    mVersionUpdateManager.upgradePatch();
                    break;
                case VersionUpdateManager.MESSAGE_UPDATE:
                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mButton = (Button) findViewById(R.id.showInfo);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LoadBugClass referenceClass = new LoadBugClass();
                Toast.makeText(MainActivity.this, referenceClass.getBugString(), Toast.LENGTH_LONG).show();
            }
        });
        checkUpdate();
    }

    private void checkUpdate() {

        String url = BASE_URL + "/MySpringWeb/mvc/getVersion";

        Log.e("check update url", url);

        OkHttpUtils
                .get()
                .url(url)
                .build()
                .execute(new StringCallback() {
                    @Override
                    public void onError(Call call, Exception e, int id) {
                    }

                    @Override
                    public void onResponse(String response, int id) {
                        Log.e("json from server", response);
                        dealData(response);
                    }
                });
    }

    private void dealData(String response) {
        try {
            JSONObject jsonObject = new JSONObject(response);
            String version_code = jsonObject.getString("version_code");
            String version_patch = jsonObject.getString("version_patch");
            String remark = jsonObject.getString("remark");
            String version_type = jsonObject.getString("version_type");
            String app_url = BASE_URL + jsonObject.getString("app_url");
            String patch_url = BASE_URL + jsonObject.getString("patch_url");
            String app_name = jsonObject.getString("app_name");
            String patch_name = jsonObject.getString("patch_name");

            mEntity = new VersionEntity(app_url, patch_url, version_code
                    , version_patch, remark, version_type,app_name,patch_name);
            Log.e("append url with ip", mEntity.toString());
            mVersionUpdateManager = VersionUpdateManager.getInstance(MainActivity.this, mEntity, mHandler);
            mVersionUpdateManager.startTask();
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onResume() {
        Log.e(TAG, "i am on onResume");

        super.onResume();
        Utils.setBackground(false);

    }

    @Override
    protected void onPause() {
        super.onPause();
        Utils.setBackground(true);
    }
}

布局:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".app.MainActivity">
    <Button  android:id="@+id/showInfo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:text="show info"/>

</RelativeLayout>

oncreate 中调用更新接口,并将解析后的内容传递给VersionUpdateManager.Handler用于处理三种消息,分别是前面提到的 app,patch ,progress 。

LoadBugClass 类

public class LoadBugClass {

    BugClass bugClass;

    public LoadBugClass() {
        bugClass = new BugClass();
    }

    public String getBugString() {
        return bugClass.bug();
    }
}

BugClass类

public class BugClass {

public BugClass() {
}
public String bug() {
    return " bug......";
}

BugClass 为 bug类,bug 返回值为bug….模拟 bug 返回值为fix …..模拟bugClass中的bug被修复。

1. 生成bug Apk

首先我们先build 编一个有bug的apk 包,为了方便测试我编的是Debug包,对于Debug包我同样进行了混淆,编译完毕之后在项目app/build/bakApk 中即可以找到编译生成的包

Android 热修复Tinker 在项目中的使用

然后我们修改Web项目中更新接口version_patch 字段为0.0

将这个有bug的apk 包按照到自己的手机上,查看效果 点击 showInfo 弹出toast 信息 bug………

Android 热修复Tinker 在项目中的使用

2. 生成patch Apk

复制bug apk名字,对项目中app下 build.gradle中tinkeroldApkPath,tinkerApplyMappingPath,tinkerApplyResourcePath 进行修改 如下图:

Android 热修复Tinker 在项目中的使用

Tinker 是通过差异比较生成的Dex apk 所以old 包路径必须的先设置一下,这样才能自动化生成差异包。

然后我们修改 BugClass的 bug 方法 返回为fix …… 模拟 bug 已经进行修复。执行生成差异包命令:

./gradlew tinkerPatchRelease // 或者 ./gradlew tinkerPatchDebug

因为我是使用Debug编译的,所以在 Terminal中执行 gradlew tinkerPatchDebug

Android 热修复Tinker 在项目中的使用

只有显示BUILD SUCCESSFUL 才算是生成差异包完成,差异包路径在/app/build/outputs/tinkerPatch/debug/下

Android 热修复Tinker 在项目中的使用

其中patch_signed_7zip.apk 就是补丁apk,复制apk 放到服务器然后将补丁号版本修改为1.0,重新运行app.

Android 热修复Tinker 在项目中的使用

SampleResultService中可根据需求进行自定义操作,比如在弹出patch success之后代码重启等……。

最后附送 app首次启动 和 再次启动 输出的日志

Android 热修复Tinker 在项目中的使用

Android 热修复Tinker 在项目中的使用

可以看到再次启动,判断Patch Version 相同就不会进行下载了。

项目下载地址http://download.csdn.net/detail/wning1/9815792