定義:指無須工程師寫代碼或者寫少量代碼就可預(yù)先自動收集用戶行為數(shù)據(jù)。
適用場景:一些通用的與業(yè)務(wù)關(guān)系不太大的場景。本文拿OnclickListener 舉例子。 其他需求比如收集頁面狀態(tài)、停留時間、方法執(zhí)行時間等通用場景也可以適用下面部分方案。
大方向:動態(tài)代理和靜態(tài)代理
動態(tài)代理:在代碼運行過程中進(jìn)行處理(動態(tài)設(shè)置各種回調(diào)事件等)
靜態(tài)代理:在代碼編譯過程中進(jìn)行處理(AspectJ /ASM/Javassist/Ast)
動態(tài)代理
各種方案基本都是通過在Application.registerActivityLifecycleCallbacks()
方案1:代理View.OnclickListener 拿到DecorView 進(jìn)行遍歷有設(shè)置OnclikListener的View 用我們的ClickListener包裝一次
關(guān)鍵代碼片段如下:
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
}
缺點:對于后續(xù)才設(shè)置的點擊事件。彈窗等 無法跟進(jìn)
方案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)
}
靜態(tài)代理
通過gradle 在編譯期間 動態(tài)插入或者修改代碼

方案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)
}
}
比如寫一個統(tǒng)計所有方法執(zhí)行時間的
@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 + " 執(zhí)行時間=" + (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();
}
更多匹配規(guī)則可以參考 https://blog.csdn.net/u013651026/article/details/130764336
缺點:只能處理我們自己代碼編譯的產(chǎn)物,無法織入第三方庫
不支持lambda語法,kotlin
AspectJX可以支持三方庫(不維護了)
方案6:ASM插樁
Transform API 在gradle8 已經(jīng)棄用
val androidComponentsExtension = target.extensions.getByType(
AndroidComponentsExtension::class.java
)
androidComponentsExtension.onVariants { variant ->
variant.transformClassesWith(TimeCostTransform::class.java, InstrumentationScope.ALL) {
}
}
// agp 8.0 之前的版本 實現(xiàn)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) // 創(chuàng)建一個新的局部變量來保存 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