过年期间闲来没事,手撸了一个辅助Android开发调试的工具App,适合Android开发者和测试同学使用。
Github地址下载, Gitee地址下载(需要登录gitee) https://gitee.com/luqinx/codecrafts-docs/raw/master/codecrafts-release-latest-version.apk
功能概览
对我这样的懒人开发者来说,反复的做同样一件事简直太煎熬了,因此我把我平时开发中需要反复操作的命令和一些繁琐的操作整理成了一个工具。
废话不多说, 先上图了解下工具的大概功能有哪些(内容比截图丰富,欢迎下载体验)
CodeCrafts的核心是一个可拖动的侧边栏的悬浮窗,悬浮窗可以折叠或展开,悬浮窗中包含5大块功能分别对应一个TAB, 这5大块功能分别是应用控制、开发者选项、常用功能,常用系统设置和全局功能
功能明细
1. 应用控制
应用控制能力将一些日常开发过程中对应用的一些繁琐的操作或者命令行指令转变为可视化的操作,而且还有自动收集和整理Crash, ANR日志,并且可以自动关联Logcat日志
文字太繁琐, 请直接看视频,地址如下
https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/898d95dad52d45af9e246b4863904cde~tplv-k3u1fbpfcp-watermark.image?
2. 开发者选项
这里的开发者选项功能是将系统的开发者选项中一些最常用的开关放在悬浮窗中, 随时启用或关闭。优势是不需要频繁去系统的开发者选项中去找对应开关,一键开闭。
我调研了其他有类似能力的工具App,都是引导用户去开发者选项中去开启或关闭功能。CodeCrafts一键开闭,无需跳转到系统开发者选项页面。
3. 最常用功能
没什么好介绍的,略。
4. 常用系统设置页面
这里承载了一些开发过程中经常需要打开的系统设置页面的快捷按钮,没什么好介绍的,略
5. 全局功能
这里的全局是相对于应用控制的,应用控制可以选择你正在开发的任意一款App, 然后应用控制中的所有能力都是对你的这个App的操作。而全局控制中的功能不针对选中的App,所有App都适用
5.1 实时数据(Realtime data)
实时数据会随着当前页面变化或者系统事件实时变化
(以上图为例介绍, 实时数据的内容不仅仅只有这些)
内容 | 含义 | 用途 |
---|---|---|
org.chromium.chrome.browser.firstrun.FirstRunActivity | 当前Activity的类名 | 代码定位 |
launch time: 208ms | 当前Activity的冷启动耗时 | 启动优化 |
com.android.chrome | 当前Activity所在应用的包名 | 常用信息 |
Chrome(uid: 10163) | 当前Activity所在应用的名称和UID | 常用信息 |
pid: 23017 | 当前Activity的进程ID | 常用信息 |
192.168.2.56,... | 当前系统的IP地址,可能有多个 | adb connect等 |
system | 当前应用是系统应用 | |
allowBackUp | 当前应用有allowBackUp属性 | 告警 |
实时数据未来还会有更多的扩展内容
5.2 不锁定屏幕
不会进入锁屏状态,也不会灭屏,避免开发过程中老是自动锁屏。
和系统开发者选项中的功能类似,区别是无论是否插入USB线都有效,开发者选项中的拔掉USB线后就无效了。都可以用,具体选择看你的使用场景。
5.3 Latest Crashes
显示缓存中最近发生的Crash的调用堆栈,可能为空也可能不止一个Crash堆栈, 需要自行查看是否是你关注的Crash。
使用说明
CodeCrafts的很多功能依赖Shell权限, 如果发现存在功能不可用的情况,一般都是shell权限获取失败了, 只需要通过在电脑终端输入adb命令"adb tcpip 5555"指令, CodeCrafts就可以自动获取shell权限了。
adb tcpip 5555
第一次使用,连接电脑终端发送"adb tcpip 5555" 或
手机断电重启,连接电脑终端发送"adb tcpip 5555" 或
莫名其妙功能不能用了,连接电脑终端发送"adb tcpip 5555"
建设中
文件沙盒, 快速浏览App的文件目录
自动化,自动化点击,输入(比如自动跳广告,自动输入账号密码?)
组件检查, 快速查看View的类型, id, 颜色等
...
后期规划
悬浮窗的tab和内容可动态配置
应用控制增加应用性能数据
提供外部SDK接口,外部应用可接入CodeCrafts进行定制化改造
CodeCrafts持续更新中...
Github地址下载, Gitee地址下载(需要登录gitee) https://gitee.com/luqinx/codecrafts-docs/raw/master/codecrafts-release-latest-version.apk
断点调试
如果你的App有多个进程,你是否被子进程如何使用断点来调试Application.onCreate()
等进程启动前期的初始化代码困扰过?
我就是被这个问题困扰过很长一段时间,起初我都是在想要断点的地方手动加上Debug.waitForDebugger()
,然后重新编译,最后再完成调试后再手动删除Debug.waitForDebugger()
。繁琐低效不说,如果一不小心把它提交到代码仓库那就很尴尬了。
CodeCrafts的断点调试可以在进程启动前,提前将进程设置为断点调试状态,这样就能解决子进程的一些初始化代码不方便调试的问题。这是Android Studio无法做到的,先看图
实现原理
原理其实很简单,就是利用am set-debug-app
, 但是set-debug-app
这个名称并不准确,导致这个命令容易被用错。
set-debug-app和clear-debug-app
set-debug-app: 设置待调试的App, 实际上这个命令的名字和描述都不太准确,应该叫set-debug-process
才对,因为set-debug-app
命令最后的参数是processName 并不是packageName。
clear-debug-app: 相反,是清除待调试的App
set-debug-app
和clear-debug-app
是am下面的一个子命令, 看下am的定义
# adb shell am
...
set-debug-app [-w] [--persistent] <PACKAGE>
Set application <PACKAGE> to debug. Options are:
-w: wait for debugger when application starts
--persistent: retain this value
clear-debug-app
Clear the previously set-debug-app.
...
为什么说set-debug-app
命令的最后的参数是processName
而不是packageName
, 看一下它在源码中的实现就知道了
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
private boolean attachApplicationLocked(@NonNull IApplicationThread thread, int pid, int callingUid, long startSeq) { ....
try {
int testMode = ApplicationThreadConstants.DEBUG_OFF;
// mDebugApp是命令后的入参,与之判断相等的是processName,而不是packageName
if (mDebugApp != null && mDebugApp.equals(processName)) {
testMode = mWaitForDebugger
? ApplicationThreadConstants.DEBUG_WAIT
: ApplicationThreadConstants.DEBUG_ON;
// app是个ProcessRecord对象,这里将这个进程打上Debugging标记
app.setDebugging(true);
if (mDebugTransient) {
mDebugApp = mOrigDebugApp;
mWaitForDebugger = mOrigWaitForDebugger;
}
}
....
}
如果你真的输入packageName
, 那么就没办法为子进程设置调试了。
CodeCrafts的断点调试实现
CodeCrafts有Shell权限,当然可以直接执行am set-debug-app
来设置要对App的哪个进程来进行断点。但这样有点简单粗暴,执行结果也很难控制。
set-debug-app
是am
下的一个子命令,因此推断它的实现肯定是在ActivityManagerService
(后面简称AMS
)中, 最终很容易找到了AMS
的setDebugApp
方法。
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
public void setDebugApp(String packageName, boolean waitForDebugger,
boolean persistent) {
enforceCallingPermission(android.Manifest.permission.SET_DEBUG_APP,
"setDebugApp()");
final long ident = Binder.clearCallingIdentity();
try {
// Note that this is not really thread safe if there are multiple
// callers into it at the same time, but that's not a situation we
// care about.
if (persistent) {
final ContentResolver resolver = mContext.getContentResolver();
Settings.Global.putString(
resolver, Settings.Global.DEBUG_APP,
packageName);
Settings.Global.putInt(
resolver, Settings.Global.WAIT_FOR_DEBUGGER,
waitForDebugger ? 1 : 0);
}
synchronized (this) {
if (!persistent) {
mOrigDebugApp = mDebugApp;
mOrigWaitForDebugger = mWaitForDebugger;
}
mDebugApp = packageName;
mWaitForDebugger = waitForDebugger;
mDebugTransient = !persistent;
if (packageName != null) {
forceStopPackageLocked(packageName, -1, false, false, true, true,
false, UserHandle.USER_ALL, "set debug app");
}
}
} finally {
Binder.restoreCallingIdentity(ident);
}
}
但是ActivityManager
(后面简称AM
)中并没有开放这个接口, 普通的App没有办法直接调用这个系统未公开的API。
那有没有间接一点的方法呢?
我们知道AM
是AMS
在应用进程中的代理,他们之间是通过Binder通信的,Binder通信一般需要定义一个aidl,IActivityManager.aidl
就是他们之间的接口定义, 而且IActivityManager.aidl
中也有定义setDebugApp
方法。
frameworks/base/core/java/android/app/IActivityManager.aidl
interface IActivityManager {
....
void setDebugApp(in String packageName, boolean waitForDebugger, boolean persistent);
....
}
IActivityManager.aidl 会在编译期间会生成
IActivityManager.java
,IActivityManager.java
中当然也会有setDebugApp
接口方法。
虽然AM没有开放setDebugApp
接口,但是根据aidl
的实现规则, IActivityManager.java
中必定有个Proxy
类,并且Proxy
必定实现了IActivityManager
接口,我们只要拿到这个Proxy
类的对象就能调用到setDebugApp
方法。
如何拿到这个Proxy
类的对象呢?
frameworks/base/core/java/android/app/ActivityManagerNative.java
public abstract class ActivityManagerNative {....
/**
* Cast a Binder object into an activity manager interface, generating
* a proxy if needed.
*
* @deprecated use IActivityManager.Stub.asInterface instead.
*/
@UnsupportedAppUsage
static public IActivityManager asInterface(IBinder obj) {
return IActivityManager.Stub.asInterface(obj);
}
/**
* Retrieve the system's default/global activity manager.
*
* @deprecated use ActivityManager.getService instead.
*/
@UnsupportedAppUsage
static public IActivityManager getDefault() {
return ActivityManager.getService();
}
....}
ActivityManagerNative提供的getDefault()方法获取的正是这个Proxy类。
虽然ActivityManagerNative和IActivityManager都是隐藏类App无法直接访问,但是我们只需要定义这两个类的同包名、同类名的文件即可解决编译问题。根据类加载双亲委派机制,会自动忽略这两个文件。这块不是重点,有空再细讲。
作者:小码哥哥
链接:https://juejin.cn/post/7201721224124629047
关注我获取更多知识或者投稿