無痕打點
打點一直是很多公司的痛點勿璃,侵入業(yè)務(wù),與業(yè)務(wù)代碼冗雜在一起推汽,不能刪除补疑,只能不停增加,很多無用的僵尸代碼就留在了業(yè)務(wù)中歹撒,而且一出問題就是重要事故莲组,因為老板要看的數(shù)據(jù)不見了。
那么是不是可以設(shè)計一套不侵入業(yè)務(wù)的打點系統(tǒng)暖夭,基于AOP的概念設(shè)計打點术徊,下面我就講講我是如何基于AOP實現(xiàn)這套打點系統(tǒng)的拒炎。
思考
一般打點事件都寫在OnClickListener接口的onClick方法中,首先能想到的是寫個OnClickListener的子類給大家用,把打點的數(shù)據(jù)也放到里面挪圾,處理過程封裝,然后要求大家都要用這個基類盲泛,可行是可行的疗隶,然而我今天主要想說的不是這種方案。
在美團的技術(shù)博客中糕韧,提到過無痕埋點的方案枫振,我主要基于這套方案進行學(xué)習(xí)喻圃。
http://tech.meituan.com/mt-mobile-analytics-practice.html?utm_source=tuicool&utm_medium=referral
AppCompatDelegate
首先想帶大家看一下AppCompatActivity的源碼,可以發(fā)現(xiàn)里面有個AppCompatDelegate對象粪滤,這個代理對象成為我們替換view的關(guān)鍵斧拍。
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (sdk >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (sdk >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (sdk >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV7(context, window, callback);
}
}
這四個類是包可見的類,依次繼承關(guān)系杖小,具體細(xì)節(jié)不講肆汹,我主要分析如何實現(xiàn)view代理的。
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
看起來也沒什么異樣予权,只是用LayoutInflater將view創(chuàng)建并添加到跟view中昂勉,其實真正的玄機出在LayoutInflater中。
在AppCompatActivity的onCreate方法中扫腺,先是這樣一段代碼:
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory(layoutInflater, this);
} else {
if (!(LayoutInflaterCompat.getFactory(layoutInflater)
instanceof AppCompatDelegateImplV7)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
這里調(diào)用LayoutInflaterCompat.setFactory將this設(shè)置進去岗照,this其實是LayoutInflaterFactory的接口,實現(xiàn)一個View onCreateView(View parent, String name, Context context, AttributeSet attrs);方法笆环。
AppCompatDelegateImplV7中對這個方法的實現(xiàn)如下:
@Override
public final View onCreateView(View parent, String name,
Context context, AttributeSet attrs) {
final View view = callActivityOnCreateView(parent, name, context, attrs);
if (view != null) {
return view;
}
return createView(parent, name, context, attrs);
}
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
final boolean isPre21 = Build.VERSION.SDK_INT < 21;
if (mAppCompatViewInflater == null) {
mAppCompatViewInflater = new AppCompatViewInflater();
}
final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, isPre21, true, isPre21);
}
public final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
final Context originalContext = context;
···
View view = null;
switch (name) {
case "TextView":
view = new AppCompatTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new AppCompatButton(context, attrs);
break;
case "EditText":
view = new AppCompatEditText(context, attrs);
break;
case "Spinner":
view = new AppCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new AppCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new AppCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new AppCompatRadioButton(context, attrs);
break;
case "CheckedTextView":
view = new AppCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new AppCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new AppCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new AppCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new AppCompatSeekBar(context, attrs);
break;
}
····
return view;
}
callActivityOnCreateView方法在AppCompatDelegateImplV14中被默認(rèn)返回空攒至,所以正常情況下,邏輯都會走到createView中躁劣,可以看到這大段的代碼似乎通過name實現(xiàn)了view的替換迫吐,接下來我們只需要看剛才setFactory之后發(fā)生了什么,是不是使用了這個方法來創(chuàng)建view账忘。
public static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
IMPL.setFactory(inflater, factory);
}
static final LayoutInflaterCompatImpl IMPL;
static {
final int version = Build.VERSION.SDK_INT;
if (version >= 21) {
IMPL = new LayoutInflaterCompatImplV21();
} else if (version >= 11) {
IMPL = new LayoutInflaterCompatImplV11();
} else {
IMPL = new LayoutInflaterCompatImplBase();
}
}
// LayoutInflater
public void setFactory(Factory factory) {
mFactorySet = true;
if (mFactory == null) {
mFactory = factory;
} else {
mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
}
}
public void setFactory2(Factory2 factory) {
mFactorySet = true;
if (mFactory == null) {
mFactory = mFactory2 = factory;
} else {
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
看源碼可以知道渠抹,這個IMPL.setFactory(inflater, factory)在API11以下調(diào)用setFactory方法,在API11以上調(diào)用setFactory2方法闪萄。這里的寫法我想大概是一些擴展及兼容性的考慮吧梧却,也使用了反射相關(guān),mFactory2是mFactory1的擴展败去,無法直接替換放航,便使用了這種兼容的方式,總之是非常復(fù)雜的圆裕。
接下來看inflate方法广鳍,任何一個xml定義的view的創(chuàng)建都會走到inflate生成,不管你是根activity的setContentView還是自己通過LayoutInflater創(chuàng)建的view吓妆,可以看到一個createViewFromTag內(nèi)部方法赊时,然后就看到了這段代碼:
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
可以看到view優(yōu)先是通過factory進行創(chuàng)建,創(chuàng)建不出來行拢,才自己進行解析的祖秒。
這樣整個流程就走通了,這套是基于所有view的創(chuàng)建,部分進行了代理竭缝,將view的創(chuàng)建替換掉房维,基于這個特性我們可以這樣來設(shè)計代碼。
設(shè)計一個自己的AppCompatDelegate對象
public class DemoAppCompatDelegate {
public static AppCompatDelegate create(Activity activity,AppCompatCallback callback) {
return create(activity, activity.getWindow(), callback);
}
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (sdk >= 23) {
return new DemoAppCompatDelegateImplV23(context, window, callback);
} else if (sdk >= 14) {
return new DemoAppCompatDelegateImplV14(context, window, callback);
} else if (sdk >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV7(context, window, callback);
}
}
}
class DemoAppCompatDelegateImplV14 extends AppCompatDelegateImplV14 {
DemoAppCompatDelegateImplV14(Context context, Window window, AppCompatCallback callback) {
super(context, window, callback);
}
@Override
View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = DataViewFactory.createView(mWindow, parent, name, context, attrs);
if (view != null) {
return view;
}
return super.callActivityOnCreateView(parent, name, context, attrs);
}
}
DataViewFactory里主要實現(xiàn)view的替換抬纸,如下:
switch (name) {
case "TextView":
return new ADCompatTextView(context, attrs);
case "ImageView":
return new ADCompatImageView(context, attrs);
case "Button":
return new ADCompatButton(context, attrs);
case "LinearLayout":
return new ADLinearLayout(context, attrs);
case "RelativeLayout":
return new ADRelativeLayout(context, attrs);
case "FrameLayout":
return new ADFrameLayout(context, attrs);
}
return null;
拿ADTextView為例:
public class ADCompatTextView extends AppCompatTextView {
public ADCompatTextView(Context context) {
super(context);
}
public ADCompatTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ADCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean performClick() {
boolean b = super.performClick();
if (b) {
ViewBindHelper.click(this);
}
return b;
}
}
public static void click(View view) {
Map<String, Object> analyse = (Map<String, Object>) view.getTag(R.id.tag_analyse);
if (analyse != null) {
//TODO 打點代碼
}
}
相當(dāng)于我把這些基礎(chǔ)的view替換掉咙俩,重寫了performClick方法,在click的時候取出tag_analyse的數(shù)據(jù)進行打點湿故。
當(dāng)你在輸出name時可以發(fā)現(xiàn)阿趁,對于自定義的view,它是輸出全路徑的坛猪,這就帶來了一個問題脖阵,難道把所有的view都寫個子類放在這里,顯然也是不現(xiàn)實的砚哆,這時候就想到了ASM方法。
所以我寫了個插件屑墨,將performClick的代碼動態(tài)的添加到view中躁锁。
ASM實現(xiàn)AOP
private void addPerformClick(ClassWriter cw) {
MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, "performClick", "()Z", null, null);
mw.visitVarInsn(ALOAD, 0);
mw.visitMethodInsn(INVOKESPECIAL, "android/view/View", "performClick", "()Z");
mw.visitVarInsn(ISTORE, 1);
mw.visitVarInsn(ILOAD, 1);
Label l1 = new Label();
mw.visitJumpInsn(IFEQ, l1);
mw.visitVarInsn(ALOAD, 0);
mw.visitMethodInsn(INVOKESTATIC, "com/example/demo/automation/ViewBindHelper", "click", "(Landroid/view/View;)V");
mw.visitLabel(l1);
mw.visitVarInsn(ILOAD, 1);
mw.visitInsn(IRETURN);
mw.visitMaxs(1, 2);
mw.visitEnd();
}
以上代碼就實現(xiàn)了編譯期向一個class文件動態(tài)添加一個重寫的performClick方法,至于我是怎么把需要替換的class文件篩選出來的卵史,大家可以參考Nuwa的插件代碼战转,這里就不都貼出來了,太多以躯。
接下來我寫篇文章記錄一下ASM的常用指令和簡單應(yīng)用槐秧。
view綁定數(shù)據(jù)
通過上述兩種方法,基本實現(xiàn)了在所有的View中添加事件攔截的方法忧设,那么還剩下怎么把數(shù)據(jù)綁定進view刁标,這部分比較具體,我不可能把我們公司的數(shù)據(jù)設(shè)計貼出來址晕,所以就簡單講一下思路膀懈。
1、首先需要一個定位標(biāo)識谨垃,頁面的任何一個view都需要一個坐標(biāo)進行標(biāo)識启搂,阿里的spm即如此,它標(biāo)識了某個頁面某個模塊某個view刘陶,至于這個標(biāo)識規(guī)則如何定義胳赌,看業(yè)務(wù)情況。
2匙隔、接下來就需要將標(biāo)識下發(fā)并實現(xiàn)與view的自動綁定疑苫,任何一個model,不管是模塊model或頁面model,一定會有個view跟它對應(yīng)缀匕,可能是某個模塊view或頁面根view纳决,這個model中可以給出一個映射關(guān)系,key用來在當(dāng)前范圍中查找到view乡小,value即剛才提到的view標(biāo)識阔加,key在安卓這邊可以通過id name的方式下發(fā),甚至可以實現(xiàn)層級id满钟,理論上任何一個有id的view都是可以被定位的胜榔。
實現(xiàn)了這套自動綁定數(shù)據(jù),那么整個無痕打點的閉環(huán)就走通了湃番,配置下發(fā)打點的方式理論上也是可以走通的夭织,有很多可以想象的空間。