Android App調試一個奇巧淫技

前言

不知道同學們有沒有遇到這些時候:

1.需要在某個時刻,獲取某個本地數據,而重新走流程debug又比較麻煩;
2.你需要臨時清理一個數據,但app當前流程,并不提供這樣的操作;
3.想在程序加段代碼,代碼要依賴app當前狀態,但不知道代碼跑不跑得通,于是在某處加入代碼,編譯運行,走流程...如果代碼失敗,還得重復上述步驟;
4.等等...

簡單地說,就是想在app運行的某個時刻,運行一小段代碼,于是不得不重新編譯、運行、走流程。當然場景還有很多很多,不一一而足。

本篇筆者介紹一個方法,可以讓你遇到這些情況,有更輕松、靈活地調試app。


一個場景

調試需求:

1.瀏覽一個app頁面,先顯示本地緩存,再請求接口更新數據,并顯示;
2.當有bug發生,需要重新走一遍這個流程debug;
3.必須先清空本地緩存,再重新走流程。

問題:

如何清空本地緩存?

方案:

1.在系統清除整個app數據,并重新登錄、走流程;
2.在代碼中加入清空緩存邏輯,重新編譯運行,并在某個事件下觸發這段邏輯,并且判斷BuildConfig.DEBUG==true才能執行(或者上線前注釋掉);
3.寫一段清空緩存代碼,能立即在app上執行,并不影響原來代碼。

探討方案:

1.方案一,最笨拙的方法,好處是不需要寫任何代碼。壞處是每次需要重新登陸、走流程。如果流程太長,花費的時間很多。

2.方案二,好處是不用重新登陸、走流程,寫一次代碼,以后調試都能用,多次調試效率相對高。壞處一,第一次調試時,比方案一多花時間,并且重新編譯運行app,這里也花費時間,并且不知道調試代碼是否有bug,如果有bug還得改代碼、編譯、運行。壞處二,調試代碼入侵到原代碼,必須小心對待,以防在上線時有影響。

3.方案三,好處是調試代碼不入侵原代碼,不需要重新編譯運行app(當然編譯調試代碼也要耗幾秒鐘)。不足,需要做一點準備工作。

方案三就是筆者今天介紹的“奇巧淫技”。


實現思路

我們的需求是:

臨時在app上跑一段代碼,并且不入侵原代碼,不需要重新編譯運行app。

實現思路:

1.調試代碼寫成單元測試(或者java main函數);
2.編譯、打包成dex文件;
3.發送dex給app;
4.app執行代碼。

相信聰明的同學,看到思路已經廓然開朗了;同時,筆者也相信很多同學直接滾到下面點demo鏈接.....下面給大家講講代碼。

代碼

1.寫調試代碼

.../test/com/example/dex,寫DexTask類,繼承Runnable

package com.example.dex;

public class DexTask implements Runnable {
    @Override
    public void run() {
        System.out.println("DexTask running...");
    }
}

run()會被app執行。

2.編譯、打包dex

例如,工程包名com.example.dex。單元測試代碼在src/main/test/java目錄。

單元測試目錄

那么,編譯后的單元測試class文件,在build/intermediates/classes/test/debug目錄。

單元測試class文件目錄

class打包jar

用shell命令,將build/intermediates/classes/test/debug/目錄打包成myjar.jar

String dir = new File("build/intermediates/classes/test/debug").getAbsolutePath();

Bash bash = new Bash();
bash.cd(dir);
bash.exec("jar -cvf myjar.jar .");

(Bash是筆者寫的一個工具類)

jar編譯成dex

使用android sdk的Dx工具命令,將myjar.jar編譯成dex.jar

> $ANDROID_HOME/build-tools/27.0.1/dx --dex --output=dex.jar myjar.jar

注意,更改目錄為sdk存在的build-tools版本dx路徑。筆者最新到27.0.1,讀者可能是其他版本(demo中會自動獲取本地最新build-tools版本)。

java代碼:

Dx     dx      = new Dx();
String dexPath = dx.dx(dir + "/myjar.jar", "dex.jar");

Dx是筆者封裝的dx工具類。

編譯jar、dex后,build/intermediates/classes/test/debug存在這兩個文件:

(demo中,每次執行完就刪掉myjar.jar和dex.jar)

3.發送dex到app

app監聽端口

app啟動一個Service,用ServerSocket監聽某端口(demo用10086端口做例子):

ServerSocket mServer = new ServerSocket(10086);
Socket       socket  = mServer.accept();

// 從socket流讀取數據,寫入本地
InputStream      is  = socket.getInputStream();
FileOutputStream fos = new FileOutputStream(context.getCacheDir() + "dex.jar");

// 詳細代碼不寫了,看demo
...

執行單元測試,發送dex文件

Socket socket = new Socket();
socket.setSoTimeout(10 * 1000);
socket.connect(new InetSocketAddress("192.168.1.*", 10086));

OutputStream os = socket.getOutputStream();

// 寫流操作,詳細代碼看demo
...

4.app執行dex代碼

app加載dex,并執行DexTask.run()

try {
    File dexFile    = new File(context.getCacheDir(), "dex.jar");
    DexClassLoader cl = new DexClassLoader(dexFile, context.getCacheDir(), null, getClassLoader());

    String taskName = "com.example.dex.DexTask";
    Class  clazz    = cl.loadClass(taskName);

    Runnable runnable = (Runnable) clazz.newInstance();
    runnable.run();
    
    // 執行完后,刪除dex文件
    dexFile.delete();
} catch (Exception e) {
    e.printStackTrace();
}

這樣幾個步驟就完成了。

調試

1.修改Working Directory

Run -> Edit Configurations -> Defaults -> Android Junit -> Working Directory 配置成 $MODULE_DIR$

2.執行單元測試RPCTest:

public class RPCTest {

    @Test
    public void rpc() throws Exception {
        Bash.DEBUG = false;

        RPC rpc = new RPC("192.168.1.154", 10086);
        rpc.remoteRun();
    }
}

RPC封裝了上述編譯、打包dex、socket發送代碼)

調試最終效果:

demo.gif

調試代碼

DexTask調試代碼,EventBus發送String:

public class DexTask implements Runnable {
    @Override
    public void run() {
        System.out.println("DexTask running...");

        EventBus.getDefault().post("收到dex并執行");
    }
}

MainActivity接受到String事件,在TextView顯示:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_hello)
    TextView tv_hello;

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

        ButterKnife.bind(this);

        EventBus.getDefault().register(this);
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMsgEvent(String msg) {
        tv_hello.setText(msg);
    }
}

demo

https://github.com/kkmike999/DexRpcDemo

demo的代碼,與本篇介紹有所出入,因為demo注重代碼解耦、可讀性,文章注重理解。


推薦閱讀:《Android 面試指南》


關于作者

我是鍵盤男。
在廣州生活,悅跑圈Android工程師,猥瑣文藝碼農。每天謀劃砍死產品經理。喜歡科學、歷史,玩玩投資,偶爾旅行。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,377評論 25 708
  • 本文已授權微信公眾號:鴻洋(hongyangAndroid)原創首發 公司的項目代碼比較多,每次調試改動java文...
    typ0520閱讀 43,723評論 56 434
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,949評論 18 139
  • 最近,因為發現學生建QQ群發不良信息(包括黃色視頻等),兒子就讀的學校開始整頓學生用智能手機的問題,也勒令學生退了...
    綠草沾裙閱讀 524評論 0 3
  • 記憶的冬至 今的夜 “今天冬至呢,下班吃餃子走”。 “不了,準備回家了,要不到我們家吃餃子走”。 “食堂肯定準備餃...
    二月219閱讀 273評論 0 1