熱修復 RocooFix篇(一)

吐槽以前先放一張大帥圖.html

(md 這張圖貌似有點小 不糾結這個了==)java

有時候項目剛剛上線或者迭代 測試或者在線上使用測出一個bug來 真讓人蛋疼 不得不從新改bug測試 打包混淆上線感受就向findviewById同樣讓你沒法忍受android

熱修復從15年開始火起來 關於熱修復的理論知識基於QQ空間熱修復的一片文章(後面我會附上這幾天學習的瞭解 不想看吐槽的能夠滑到最後面 沒辦法爲了湊字git

數不夠150個字數不容許發表 難道這就能夠阻擋我吐槽的 呸 是學習的熱情了嗎)github

其實在學習熱修復以前 咱們仍是有必要了解一下熱修復的原理 下面開始正經(後面均有連接):web

熱修復大體分爲兩種解決方式:json

PathClassLoader:windows

   

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集成使用 謝謝你們。

相關文章
相關標籤/搜索