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

来源:互联网 发布:php代码加密 编辑:程序博客网 时间:2024/06/06 03:46

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

QQ团队基于android dex分包方案提出的热修复方案,代表:Nuwa , HotfixAlibaba 提出的热修复方案,代表: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的版本号来获取TinkerIddef 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 中即可以找到编译生成的包

这里写图片描述

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

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

这里写图片描述

2. 生成patch Apk

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

这里写图片描述

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

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

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

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

这里写图片描述

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

这里写图片描述

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

这里写图片描述

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

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

这里写图片描述

这里写图片描述

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

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

0 0
原创粉丝点击