热修复 RocooFix篇(一)
吐槽之前先放一张大帅图.
(md 这张图貌似有点小 不纠结这个了==)
有时候项目刚刚上线或者迭代 测试或者在线上使用测出一个bug来 真让人蛋疼 不得不重新改bug测试 打包混淆上线感觉就向findviewById一样让你无法忍受
热修复从15年开始火起来 关于热修复的理论知识基于QQ空间热修复的一片文章(后面我会附上这几天学习的了解 不想看吐槽的可以滑到最后面 没办法为了凑字
数不够150个字数不允许发表 难道这就可以阻挡我吐槽的 呸 是学习的热情了吗)
其实在学习热修复之前 我们还是有必要了解一下热修复的原理 下面开始正经(后面均有链接):
热修复大致分为两种解决方式:
PathClassLoader:
DexClassLoader:
官方文档说的很明白了:(我也没看明白 接着查==)
参考一下stack overflow的回答:
两者的区别PathClassLoader只能加载本地的classes 而DexClassLoader可以加载apk或者jar压缩包中的dex文件
需要注意的是:
就是说DexClassLoader不提倡从sd卡加载 ,(This class loader requires an application-private, writable directory to cache optimized classes. Use Context.getCodeCacheDir()
to create such a directory: 需要私人的,可写的目录缓存优化类 也是一些热修复一些生成jar 需要md5加密的原因?)关于这个问题 农民伯伯11年就写了一篇博文:
Android动态加载jar/dex :http://www.cnblogs.com/over140/archive/2011/11/23/2259367.html
以及这位 后来没坚持写博客了 http://blog.csdn.net/quaful/article/details/6096951
(这里忍不住吐槽一下: 我靠 农名伯伯是有多屌啊!可惜不能发表情包 吓得我下巴就要掉下来了!应用这么多图和链接 只是帮助我们了解一下 现在大部分博文不严谨 百度一下全是一样的。写到这里感觉 如果接着写下去的话 这个题目要改为热修复初探! 当然不! 分享才知道自己的不足)
...关于热修复 目前大概有以下几种(不严谨 有错误欢迎补充 谢谢. 关于以下几种分别附上链接 以及 demo个人使用总结 部分还在学习中):
基于Xposed的AOP框架的淘宝的 Dexposed,支付宝的AndFix,QZone的超级热补丁方案(Nuwa 采用了相同的方式),以及微信的Tinker,
RocooFix(是基于扣扣空间的方案,稳不稳定看下面微信对qq空间方案的评价 本篇后面使用Tomat 完整演示),饿了么的Amigo,以及掌阅的Zeus Plugin等等吧。
================以上内容 是初探 ===========================
关于RocooFix
在使用RocooFix之前 我们很快找到两种方法:
静态修复:
RocooFix.applyPatch(Context context, String dexPath);
动态修复:
RocooFix.applyPatchRuntime(Context context, String dexPath);
思路:
出现bug之后 我们使用RocooFix集成 生成patch.jar文件 给后台让其上传到服务器(获取向后台要三个接口 一个上传patch.jar文件 一个用来修改json数据
一个用来获取到json数据)
静态修复的话 我们 直接从服务器拿到jar数据 放在sdcard某个位置 app重启自动修复
动态修复的话 我们可能要多思考几步 就是动态修复完成之后 如何避免重复 下载 需要json权限判断 (代码中会具体有)
如果同时使用静态修复和动态修复的话 可能会崩溃
Tomcat 解压之后在webapps目录下新建文件夹(我的是HotFix) .复制\ROOT目录下 WEB-INF,丢进HotFix中。
patch.jar是我们在第二次编译Android Studio version 2 debug目录下生成的jar文件 我们复制到HotFix 目录下
并且新建b.txt文件 存储json字符串 下载到本地:
{"md5":"json","patch":"10.0.2.2/HotFix/patch.jar","Root":"wifi","download":"ok"}
md5 用于txt文本加密 patch模拟服务器patch.jar生成的位置 root 个人感觉是否需要判断 在wifi条件下自动修复 download 本地下载txt文件之后 判断是否是ok 如果ok 动态修复 修复之后 将ok 的值改为其他值 覆盖sdcard的txt文件 避免静态修复 之后启用动态修复 这样会崩掉
具体见代码:
App文件:
package com.example.administrator.myapplication;
import android.app.Application;
import android.content.Context;
import android.os.Environment;
import android.util.Log;
import com.dodola.rocoofix.RocooFix;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
* Created by One on 2016/8/31.
*/
public class App extends Application {
private String path = "/One1988/data";//项目目录
private String urlTest = "http://10.0.2.2/HotFix/b.txt";//Tomcat上面的 text文件json数据
private String patchPath = Environment.getExternalStorageDirectory().getAbsolutePath() +
path + "/patch.jar";//jar文件 位于 sdcard/One1988/data 目录下
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
RocooFix.init(base);
try {
if (patchPath != null) {
File file = new File(patchPath);
if (file.exists()) {
/**
* 存在的话修复从制定目录加载静态修复
*/
try {
RocooFix.applyPatch(base, patchPath);
} catch (Exception e) {
Log.d("file.exist", "热修复出现异常: ");
}
} else {
/**
* 没有的话下载写入 读取
*/
String apath = Environment.getExternalStorageDirectory().getAbsolutePath() + path;
File fileP = new File(apath);
if(!fileP.getParentFile().exists()){
fileP.getParentFile().mkdirs();
}
getHttp(urlTest);
}
}
} catch (Exception e) {
Log.d("RocooFix热修复出现问题", e.toString());
}
}
/**
* 下载写入sdcard json数据 用于控制
*/
OkHttpClient mOkHttpClient = new OkHttpClient();
private void getHttp(String url) {
Request request = new Request.Builder()
.url(url)
.build();
mOkHttpClient.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
writeSdcard(response, "download.txt");//写入txt文件
}
}
});
}
/**
* 写入txt文件进sdcard实际考虑加密什么的
* @param response
*/
private void writeSdcard(Response response, String filePath) {
InputStream is = null;
byte[] buf = new byte[2048];
int len = 0;
FileOutputStream fos = null;
String SDPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/One1988/data";
try {
is = response.body().byteStream();
File file = new File(SDPath, filePath);
fos = new FileOutputStream(file);
long sum = 0;
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);
sum += len;
}
fos.flush();
Log.d("文件下载", "txt文件下载成功");
} catch (Exception e) {
Log.d("文件下载", "文件下载失败");
} finally {
try {
if (is != null)
is.close();
} catch (IOException e) {
}
try {
if (fos != null)
fos.close();
} catch (IOException e) {
}
}
}
}
MainActivity文件:package com.example.administrator.myapplication;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.Button;
import android.widget.Toast;
import com.dodola.rocoofix.RocooFix;
import com.example.administrator.myapplication.bean.FileUtils;
import org.json.JSONObject;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initUi();
}
/**
* 测试Ui
*/
private String patchPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/One1988/data/patch.jar";
private void initUi() {
Button button = (Button) findViewById(R.id.button);
Button buttonRun = (Button) findViewById(R.id.buttonRun);
final Test test = new Test();
/**
* 测试立即生效
*/
button.setOnClickListener(v -> {
Toast.makeText(MainActivity.this, test.show(), Toast.LENGTH_SHORT).show();
});
/**
* button run测试
*/
buttonRun.setOnClickListener(v -> {
try {
TestRun run = new TestRun();
String json = read();
JSONObject obj = new JSONObject(json);
String download = obj.optString("download");
Log.d("download字段", "json: " + json);
Log.d("download字段", "download: " + download);
if (download.equals("ok")) {
//下载修复
//getHttp(urlPatch);
updateJson();
} else {
Toast.makeText(this, run.run(), Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
/**
* 插件位于sdcard位置
*/
private String urlPatch = "http://10.0.2.2/HotFix/patch.jar";
/**
* 读取字符串
* @return
* @throws Exception
*/
private String read() throws Exception {//读取sdcard中的json字符串
String SDPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/One1988/data";
File file = new File(SDPath, "/download.txt");
StringBuffer sb = new StringBuffer();
FileInputStream fis = new FileInputStream(file);
int c;
while ((c = fis.read()) != -1) {
sb.append((char) c);
}
fis.close();
String fileString = sb.toString();
Log.d("FileInputStream:", "file: " + sb.toString());
return fileString;
}
/**
* Test 测试
*/
public class Test {
public String show(){
return "出现bug!";
//return "来点不一样的!";
}
}
/**
* 测试
*/
public class TestRun {
public String run(){
return "测试修复前!";
//return "bug已经修复了欧耶!";
}
}
/**
* 下载jar文件立即修复
*/
private void writeSdcard(Response response, String filePath) {
InputStream is = null;
byte[] buf = new byte[2048];
int len = 0;
FileOutputStream fos = null;
String SDPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/One1988/data";
try {
is = response.body().byteStream();
File file = new File(SDPath, filePath);
fos = new FileOutputStream(file);
long sum = 0;
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);
sum += len;
}
fos.flush();
Log.d("文件下载", "jar文件下载成功");
RocooFix.applyPatchRuntime(this, patchPath);
mHandler.sendEmptyMessage(0);
} catch (Exception e) {
Log.d("文件下载", "文件下载失败");
} finally {
try {
if (is != null)
is.close();
} catch (IOException e) {
}
try {
if (fos != null)
fos.close();
} catch (IOException e) {
}
}
}
/**
* 下载
*/
OkHttpClient mOkHttpClient = new OkHttpClient();
private void getHttp(String url) {
Request request = new Request.Builder()
.url(url)
.build();
mOkHttpClient.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
writeSdcard(response, "patch.jar");
}
}
});
}
/**
* Handler
*/
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case 0:
TestRun run = new TestRun();
Toast.makeText(MainActivity.this, run.run(), Toast.LENGTH_SHORT).show();
/**
* 我的想法是:
*/
try {
String json = read();
if(json.contains("ok")){
json.replaceAll("ok","nohttp");
}
File file = new File(patchPath);
if(file.exists()){
FileUtils.writeFileFromString(file,json,false);
}
Log.d("读取到的字符串:", "handleMessage: "+read());
} catch (Exception e) {
e.printStackTrace();
}
break;
default:
break;
}
}
};
/**
* 测试更改sdcard中的字符串
*/
String download = Environment.getExternalStorageDirectory().getAbsolutePath() + "/One1988/data/download.txt";
private void updateJson() {
try {
String read = read();
Log.d("写入字符串", "之前: "+read);
/*Gson gson = new Gson();
Bean bean = gson.fromJson(read, Bean.class);
if(bean.getDownload().equals("ok")){
bean.setDownload("哇咔咔");
}
String write = gson.toJson(bean);*/
/**
* 我尝试用上面方法修改json字符串但是 加入gson依赖之后
* 添加混淆会出错 这种比较麻烦用于学习 后面会学习一下其他的几种方式
* 选择一种最好的混淆方式 如果这里你解决了 请告诉我 谢谢!
*/
JSONObject object = new JSONObject();
object.put("md5","json");
object.put("patch","10.0.2.2/HotFix/patch.jar");
object.put("Root","wifi");
object.put("download","成功修改后!");
String write = String.valueOf(object);
Log.d("写入字符串", "之前: ======");
Log.d("写入字符串", "之前: "+write);
write(write,download);
Log.d("写入字符串", "之后: "+read());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 写入字符串到txt文件
* @param toSaveString
* @param filePath
*/
public static void write(String toSaveString, String filePath) {
try{
File saveFile = new File(filePath);
if(!saveFile.exists()){
File dir = new File(saveFile.getParent());
dir.mkdirs();
saveFile.createNewFile();
}
FileOutputStream outputStream = new FileOutputStream(saveFile);
outputStream.write(toSaveString.getBytes());
outputStream.close();
}catch (Exception e){
Log.d("字符串写入失败", "saveFile: "+e.toString());
}
}
}其他配置 等具体见demo这种比较麻烦的是 考虑混淆问题 后面学习Amigo的使用比较几种热修复的优点。记录一下。
QQ空间终端开发团队:(引发热潮的一篇文章 文中的图片我就不引用了 大神请绕道)
https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a
PathClassLoader和DexClassLoader官方文档:
https://developer.android.com/reference/dalvik/system/PathClassLoader.html
https://developer.android.com/reference/dalvik/system/DexClassLoader.html
stack overflow 上关于PathClassLoader和DexClassLoader的不同
http://stackoverflow.com/questions/37296192/what-are-differences-between-dexclassloader-and-pathclassloader
饿了么:https://github.com/eleme/Amigo
掌阅:https://github.com/iReaderAndroid/ZeusPlugin
女娲: https://github.com/jasonross/Nuwa
RocooFix:https://github.com/dodola/RocooFix
Tomact 8.5.4 windows 64:
http://download.csdn.net/detail/onebelowzero2012/9626103
demo 以及txt文件:
http://download.csdn.net/detail/onebelowzero2012/9628341
最后:欢迎给出意见 一起学习 加入群Android&Go,Let's go! 521039620 (感觉自己像个拉皮条的 ==)
=======2017/06/06 补记 现在回过头来看 感觉写的太浅 希望不要误导读者 自己多尝试 关于修复这块 建议大家用微信Tinker 或者 参考腾讯Bugly 的Tinker集成使用 谢谢大家。