原方案:
- 添加原框架maven依赖,并在activity中调用init方法。
- 在运行时动态生成View的hook dex,在加载到内存中。
新方案:
使用JVMTI监听Debug应用的方法回调。
优点:
- 不需要在apk里面添加依赖,即硬编码,通过adb命令即插即用。
- 不生成代理View,减少内存占用
缺点:
- 仅支持Android8.0以上的Debug应用
1)选择当前应用对应arm版本的 SO
文件位置:SO文件地址
2)通过AS左侧的Device File Explorer工具,将SO
文件放入到当前待测试应用的私有目录。
以当前demo工程举例,放入文件位置为:
/data/data/com.spearbothy.simpletouch/libsimple_touch.so
上步骤2),因为手机没有root,暂时没有找到合适的adb命令直接push...
adb shell am start --attach-agent /data/data/com.spearbothy.simpletouch/libsimple_touch.so -S com.spearbothy.simpletouch/.MainActivity
// 线程#全路径类名+hashCode#方法名#ActionMode#方法走向(>>调用 << 返回) 返回值
I/Simple_Touch: main#Lcom/android/internal/policy/DecorView;185412568#dispatchTouchEvent()#ACTION_DOWN >>
I/Simple_Touch: main#Landroidx/appcompat/app/AppCompatDelegateImpl$AppCompatWindowCallback;151497521#dispatchTouchEvent()#ACTION_DOWN >>
I/Simple_Touch: main#Lcom/spearbothy/simpletouch/MainActivity;66354895#dispatchTouchEvent()#ACTION_DOWN >>
I/Simple_Touch: main#Lcom/android/internal/policy/DecorView;185412568#dispatchTouchEvent()#ACTION_DOWN >>
I/Simple_Touch: main#Lcom/android/internal/policy/DecorView;185412568#onInterceptTouchEvent()#ACTION_DOWN >>
I/Simple_Touch: main#Lcom/android/internal/policy/DecorView;185412568#onInterceptTouchEvent()#ACTION_DOWN << false
I/Simple_Touch: main#Landroid/widget/LinearLayout;122880534#dispatchTouchEvent()#ACTION_DOWN >>
I/Simple_Touch: main#Landroid/widget/LinearLayout;122880534#onInterceptTouchEvent()#ACTION_DOWN >>
I/Simple_Touch: main#Landroid/widget/LinearLayout;122880534#onInterceptTouchEvent()#ACTION_DOWN << false
I/Simple_Touch: main#Landroid/widget/FrameLayout;201246615#dispatchTouchEvent()#ACTION_DOWN >>
I/Simple_Touch: main#Landroid/widget/FrameLayout;201246615#onInterceptTouchEvent()#ACTION_DOWN >>
I/Simple_Touch: main#Landroid/widget/FrameLayout;201246615#onInterceptTouchEvent()#ACTION_DOWN << false
I/Simple_Touch: main#Landroidx/appcompat/widget/ActionBarOverlayLayout;34942340#dispatchTouchEvent()#ACTION_DOWN >>
I/Simple_Touch: main#Landroidx/appcompat/widget/ActionBarOverlayLayout;34942340#onInterceptTouchEvent()#ACTION_DOWN >>
I/Simple_Touch: main#Landroidx/appcompat/widget/ActionBarOverlayLayout;34942340#onInterceptTouchEvent()#ACTION_DOWN << false
I/Simple_Touch: main#Landroidx/appcompat/widget/ContentFrameLayout;251316589#dispatchTouchEvent()#ACTION_DOWN >>
I/Simple_Touch: main#Landroidx/appcompat/widget/ContentFrameLayout;251316589#onInterceptTouchEvent()#ACTION_DOWN >>
I/Simple_Touch: main#Landroidx/appcompat/widget/ContentFrameLayout;251316589#onInterceptTouchEvent()#ACTION_DOWN << false
I/Simple_Touch: main#Landroid/widget/LinearLayout;24455586#dispatchTouchEvent()#ACTION_DOWN >>
I/Simple_Touch: main#Landroid/widget/LinearLayout;24455586#onInterceptTouchEvent()#ACTION_DOWN >>
I/Simple_Touch: main#Landroid/widget/LinearLayout;24455586#onInterceptTouchEvent()#ACTION_DOWN << false
I/Simple_Touch: main#Lcom/spearbothy/simpletouch/CustomView;115230259#dispatchTouchEvent()#ACTION_DOWN >>
I/Simple_Touch: main#Lcom/spearbothy/simpletouch/CustomView;115230259#onTouchEvent()#ACTION_DOWN >>
I/Simple_Touch: main#Lcom/spearbothy/simpletouch/CustomView;115230259#onTouchEvent()#ACTION_DOWN << true
I/Simple_Touch: main#Lcom/spearbothy/simpletouch/CustomView;115230259#dispatchTouchEvent()#ACTION_DOWN << true
I/Simple_Touch: main#Landroid/widget/LinearLayout;24455586#dispatchTouchEvent()#ACTION_DOWN << true
I/Simple_Touch: main#Landroidx/appcompat/widget/ContentFrameLayout;251316589#dispatchTouchEvent()#ACTION_DOWN << true
I/Simple_Touch: main#Landroidx/appcompat/widget/ActionBarOverlayLayout;34942340#dispatchTouchEvent()#ACTION_DOWN << true
I/Simple_Touch: main#Landroid/widget/FrameLayout;201246615#dispatchTouchEvent()#ACTION_DOWN << true
I/Simple_Touch: main#Landroid/widget/LinearLayout;122880534#dispatchTouchEvent()#ACTION_DOWN << true
I/Simple_Touch: main#Lcom/android/internal/policy/DecorView;185412568#dispatchTouchEvent()#ACTION_DOWN << true
I/Simple_Touch: main#Lcom/spearbothy/simpletouch/MainActivity;66354895#dispatchTouchEvent()#ACTION_DOWN << true
I/Simple_Touch: main#Landroidx/appcompat/app/AppCompatDelegateImpl$AppCompatWindowCallback;151497521#dispatchTouchEvent()#ACTION_DOWN << true
I/Simple_Touch: main#Lcom/android/internal/policy/DecorView;185412568#dispatchTouchEvent()#ACTION_DOWN << true
核心原理及文档
其余相关文档可搜索JVMTI等查询。
具体源码:Agent_OnAttach
extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char *options, void *reserved) {
ALOGI("Agent_OnAttach options %s", options);
// 创建jvmti指针
localJvmtiEnv = CreateJvmtiEnv(vm);
if (localJvmtiEnv == nullptr) {
ALOGI("Agent_OnAttach jvmti_env err");
return JNI_ERR;
}
// 开启调试功能
SetAllCapabilities(localJvmtiEnv);
// 设置方法进入&方法退出的回调方法,回调至JvmtiCallbacks
jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.MethodEntry = &JvmtiCallbacks::methodEntry;
callbacks.MethodExit = &JvmtiCallbacks::methodExit;
int error = localJvmtiEnv->SetEventCallbacks(&callbacks, sizeof(callbacks));
ALOGI("Agent_OnAttach_callbacks SetEventCallbacks result = %d", error);
// 设置监听的事件:方法进入&方法退出
SetEventNotification(localJvmtiEnv, JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY);
SetEventNotification(localJvmtiEnv, JVMTI_ENABLE, JVMTI_EVENT_METHOD_EXIT);
return JNI_OK;
}
具体源码:jvmti_callbacks.cpp
回调方法的签名参数如下:
static void JNICALL methodEntry(jvmtiEnv *jvmti_env, // jvmti指针
JNIEnv *jni_env, // jni指针
jthread thread, // 所在线程
jmethodID method // 方法标识
);
static void JNICALL methodExit(jvmtiEnv *jvmti_env, // jvmti 指针
JNIEnv *jni_env, // jni指针
jthread thread, // 所在线程
jmethodID method, // 方法标识
jboolean was_popped_by_exception, // 是否是因为异常退出
jvalue return_value // 返回值
);
在回调方法中,我们结合jvmti
指针&jni
指针暴露的方法,获取更多信息,具体信息如下:
// 获取方法签名
char *methodName;
char *methodSignature;
jvmti_env->GetMethodName(method, &methodName, &methodSignature, NULL);
// 根据方法签名判断是否是 onTouchEvent等
if (!Utils::getInstance()->isTargetMethod(methodName)) {
return;
}
// 获取线程信息
char *threadName;
Utils::getInstance()->getThreadName(jvmti_env, thread, &threadName);
// 获取本地变量表,用于获取方法入参
jint entry_count = 0;
jvmtiLocalVariableEntry *table_ptr = NULL;
jvmti_env->GetLocalVariableTable(method, &entry_count, &table_ptr);
const char *motionEvent;
for (int j = 0; j < entry_count; j++) {
if (strstr(table_ptr[j].signature, "MotionEvent") != NULL) {
jobject param_obj;
// 根据本地变量表获取到的索引获取入参对象,即MotionEvent对象
jvmti_env->GetLocalObject(thread, 0, table_ptr[j].slot, ¶m_obj);
// 调用MotionEvent对象的actionToString方法,获取ACTION_DOWN等事件类型。
motionEvent = jni_env->GetStringUTFChars(
Utils::getInstance()->motionEvent_actionToString(jni_env, param_obj), 0);
// 释放资源
jni_env->DeleteLocalRef(param_obj);
jvmti_env->Deallocate(reinterpret_cast<unsigned char *>(table_ptr[j].signature));
jvmti_env->Deallocate(reinterpret_cast<unsigned char *>(table_ptr[j].name));
jvmti_env->Deallocate(
reinterpret_cast<unsigned char *>(table_ptr[j].generic_signature));
}
}
// 获取调用当前方法的对象
jobject instance;
jvmti_env->GetLocalInstance(thread, 0, &instance);
// 获取当前对象的全路径类名
jclass klass = jni_env->GetObjectClass(instance);
char *className;
jvmti_env->GetClassSignature(klass, &className, NULL);
// 获取对象的hashCode
jint hashCode;
jvmti_env->GetObjectHashCode(instance, &hashCode);
// 根据信息组装,输出日志
// ...
methodEntry
& methodExit
的回调,是以单一方法粒度回调,例如子类实现了onTouchEvent()
,并在方法内部调用了super.onTouchEvent()
,则进入和进出对子类和父类都会回调,即共四次。
但对于当前工具,期望是以对象为粒度,即只关注某一个对象最终对事件分发的处理结果,因此加了一些代码兼容,具体如下:
if (is_entry) {
// 如果当前进入对象和方法和前一个相同,则只输出第一个
string lastMethod = string(className) + to_string(hashCode) + string(methodName);
if (!methodList.empty()) {
string preMethod = methodList.back();
if (lastMethod != preMethod) {
ALOGI("%s#%s%d#%s()#%s >>", threadName, className, hashCode, methodName,
motionEvent);
}
} else {
ALOGI("%s#%s%d#%s()#%s >>", threadName, className, hashCode, methodName, motionEvent);
}
methodList.push_back(lastMethod);
} else {
// 如果退出的对象和方法和前一个退出相同,则只输出最后一个
methodList.pop_back();
string lastMethod = methodList.back();
string curMethod = string(className) + to_string(hashCode) + string(methodName);
if (curMethod != lastMethod) {
ALOGI("%s#%s%d#%s()#%s << %s", threadName, className, hashCode, methodName, motionEvent,
return_value ? "true" : "false");
}
}