定义:指无须工程师写代码或者写少量代码就可预先自动收集用户行为数据。
适用场景:一些通用的与业务关系不太大的场景。本文拿OnclickListener 举例子。 其他需求比如收集页面状态、停留时间、方法执行时间等通用场景也可以适用下面部分方案。
大方向:动态代理和静态代理
动态代理:在代码运行过程中进行处理(动态设置各种回调事件等)
静态代理:在代码编译过程中进行处理(AspectJ /ASM/Javassist/Ast)
动态代理
各种方案基本都是通过在Application.registerActivityLifecycleCallbacks()
方案1:代理View.OnclickListener 拿到DecorView 进行遍历有设置OnclikListener的View 用我们的ClickListener包装一次
关键代码片段如下:
class WrapOnclickListener : View.OnClickListener {
private var originClickListener: View.OnClickListener? = null
constructor(originClickListener: View.OnClickListener?) {
this.originClickListener = originClickListener
}
override fun onClick(v: View?) {
try {
originClickListener?.onClick(v)
if (v != null) {
TrackApi.track(v)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun getOnclickListener(view: View): View.OnClickListener? {
val hasOnClickListeners = view.hasOnClickListeners()
if (hasOnClickListeners) {
try {
val viewClass = Class.forName("android.view.View")
val declaredMethod = viewClass.getDeclaredMethod("getListenerInfo")
declaredMethod.isAccessible = true
val listenerInfo = declaredMethod.invoke(view)
val onClickFiled = Class.forName("android.view.View\$ListenerInfo")
.getDeclaredField("mOnClickListener")
onClickFiled.isAccessible = true
return onClickFiled.get(listenerInfo) as View.OnClickListener
} catch (e: Exception) {
e.printStackTrace()
}
}
return null
}
缺点:对于后续才设置的点击事件。弹窗等 无法跟进
方案2:代理Window.Callback
在dispatchTouchEvent中处理点击view
var downView:View?=null
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
Log.d(TAG, "dispatchTouchEvent() called with: event = $event")
val decorView = activity!!.window.decorView
if (event?.action == MotionEvent.ACTION_DOWN) {
// 找到按下View
downView= getTargetView(decorView,event)?.last()
}
if (downView!=null&&event?.action == MotionEvent.ACTION_UP) {
// 查看松手的View
val targetView = getTargetView(decorView,event)?.last()
if (targetView!=null&&targetView==downView) {
TrackApi.track(downView!!)
}
}
return originCallback?.dispatchTouchEvent(event) == true
}
方案3:代理View.AccessibilityDelegate 和方案一类似。
反射获取View的 mAccessibilityDelegate
fun getViewAccessibility(view: View): AccessibilityDelegate? {
var delegate: View.AccessibilityDelegate? = null
try {
val kClass: Class<*> = view.javaClass
val method = kClass.getMethod("getAccessibilityDelegate")
delegate = method.invoke(view) as View.AccessibilityDelegate
} catch (e: Exception) {
e.printStackTrace()
}
if (delegate == null || delegate !is MyClickDelegate) {
view.setAccessibilityDelegate(MyClickDelegate(delegate))
}
return null
}
class MyClickDelegate : View.AccessibilityDelegate {
var realAccessibilityDelegate: View.AccessibilityDelegate? = null
constructor(realAccessibilityDelegate: View.AccessibilityDelegate?) : this() {
this.realAccessibilityDelegate = realAccessibilityDelegate
}
constructor()
override fun sendAccessibilityEvent(host: View, eventType: Int) {
super.sendAccessibilityEvent(host, eventType)
realAccessibilityDelegate?.sendAccessibilityEvent(host, eventType)
TrackApi.track(host)
}
}
方案4:添加透明层 处理onTouchEvent
private fun addAlphaFramlayout(activity: Activity,decorView: ViewGroup) {
val alphaFrameLayout = AlphaFrameLayout(activity)
decorView.addView(alphaFrameLayout, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
ViewCompat.setElevation(alphaFrameLayout,999f)
}
静态代理
通过gradle 在编译期间 动态插入或者修改代码
方案5:AspectJ AOP
val variants = project.android.applicationVariants
variants.all {
println("ajc args variants: ${this.toString()} javaCompileProvider=${javaCompileProvider.get()}")
javaCompileProvider.get().doLast {
val args = arrayOf(
"-showWeaveInfo",
"-1.7",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.joinToString(File.pathSeparator)
)
println("ajc args: ${args.contentToString()}")
val messageHandler = MessageHandler(true)
Main().run(args, messageHandler)
}
}
比如写一个统计所有方法执行时间的
@Aspect
public class TrackUtils {
private static final String TAG = "TrackUtils";
@Around("execution(* *(..))")
public Object checkAllMethod(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.nanoTime();
Object proceed = joinPoint.proceed();
long endTime = System.nanoTime();
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
Log.d(TAG, "checkAllMethod() called with: joinPoint = " + joinPoint + " startTime=" + startTime + " endTime=" + endTime + " 执行时间=" + (endTime - startTime) + " signature=" + signature + " method=" + method + "");
return proceed;
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initView();
}
private void initView() {
}
处理后
protected void onCreate(@Nullable Bundle savedInstanceState) {
JoinPoint var3 = Factory.makeJP(ajc$tjp_0, this, this, savedInstanceState);
onCreate_aroundBody1$advice(this, savedInstanceState, var3, TrackUtils.aspectOf(), (ProceedingJoinPoint)var3);
}
private void initView() {
JoinPoint var1 = Factory.makeJP(ajc$tjp_1, this, this);
initView_aroundBody3$advice(this, var1, TrackUtils.aspectOf(), (ProceedingJoinPoint)var1);
}
static {
ajc$preClinit();
}
更多匹配规则可以参考 https://blog.csdn.net/u013651026/article/details/130764336
缺点:只能处理我们自己代码编译的产物,无法织入第三方库
不支持lambda语法,kotlin
AspectJX可以支持三方库(不维护了)
方案6:ASM插桩
Transform API 在gradle8 已经弃用
val androidComponentsExtension = target.extensions.getByType(
AndroidComponentsExtension::class.java
)
androidComponentsExtension.onVariants { variant ->
variant.transformClassesWith(TimeCostTransform::class.java, InstrumentationScope.ALL) {
}
}
// agp 8.0 之前的版本 实现transform
// val appExtension = target.extensions.getByType(AppExtension::class.java)
// appExtension.registerTransform(ASMAOPTransform())
private var startTimeLocal = -1 // 保存 startTime 的局部变量索引
override fun onMethodEnter() {
super.onMethodEnter()
println("onMethodEnter access=$access name=$name descriptor=$descriptor signature=$signature exceptions=$exceptions")
// 在onMethodEnter中插入代码 val startTime = System.currentTimeMillis()
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"java/lang/System",
"currentTimeMillis",
"()J",
false
)
startTimeLocal = newLocal(Type.LONG_TYPE) // 创建一个新的局部变量来保存 startTime
mv.visitVarInsn(Opcodes.LSTORE, startTimeLocal)
}
override fun onMethodExit(opcode: Int) {
// 在onMethodExit中插入代码 Log.e("tag", "Method: $name, timecost: " + (System.currentTimeMillis() - startTime))
mv.visitTypeInsn(
Opcodes.NEW,
"java/lang/StringBuilder"
);
mv.visitInsn(Opcodes.DUP);
mv.visitLdcInsn("PluginThread: $pluginExecuteThreadName Method: $name, methodTime: ");
mv.visitMethodInsn(
Opcodes.INVOKESPECIAL,
"java/lang/StringBuilder",
"<init>",
"(Ljava/lang/String;)V",
false
);
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"java/lang/System",
"currentTimeMillis",
"()J",
false
);
mv.visitVarInsn(Opcodes.LLOAD, startTimeLocal);
mv.visitInsn(Opcodes.LSUB);
mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuilder",
"append",
"(J)Ljava/lang/StringBuilder;",
false
);
mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuilder",
"toString",
"()Ljava/lang/String;",
false
);
mv.visitLdcInsn("TimeCostClassVisitor")
mv.visitInsn(Opcodes.SWAP)
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"android/util/Log",
"e",
"(Ljava/lang/String;Ljava/lang/String;)I",
false
)
mv.visitInsn(POP)
super.onMethodExit(opcode)
println("opcode = [${opcode}]")
println("onMethodExit access=$access name=$name descriptor=$descriptor signature=$signature exceptions=$exceptions")
}
神策埋点插件地址:https://github.com/sensorsdata/sa-sdk-android-plugin2