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工程师,猥琐文艺码农。每天谋划砍死产品经理。喜欢科学、历史,玩玩投资,偶尔旅行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容