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
- Android 热修复Tinker 在项目中的使用
- Android 热修复Tinker 在项目中的使用
- Android热修复 Tinker
- Android热修复Tinker
- android 热修复 Tinker
- Android Tinker热修复
- Android热修复之Tinker使用初探
- android tinker 热修复使用及注意事项
- Android架构(二)热修复技术Tinker在Android中的实践
- Android热修复之Tinker
- Android 热修复 Tinker接入
- Android 热修复 Tinker 接入
- 热修复Tinker简单使用
- Tinker热修复技术使用
- 热修复tinker的使用
- Android热修复之微信Tinker使用初探
- [Android]腾讯Tinker热修复框架简单使用
- android使用tinker对app进行热修复
- 文章标题
- Android启动欢迎界面前黑屏或白屏完美解决办法
- (codeforces)C. Socks
- Linux开机启动流程详细步骤是什么
- 关于RecyclerViewHeader实现
- Android 热修复Tinker 在项目中的使用
- 对象序列化
- php ajax跨域问题
- eclipse的安装及tomcat的配置
- 796C
- Java实现分页数据获取CachedRowSet
- recyclerview历史
- 进程间通信方式总结——共享内存
- Django账号绑定邮箱时发送链接