無痕打點研究

無痕打點

打點一直是很多公司的痛點勿璃,侵入業(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ā)打點的方式理論上也是可以走通的夭织,有很多可以想象的空間。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末吠撮,一起剝皮案震驚了整個濱河市尊惰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌泥兰,老刑警劉巖弄屡,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鞋诗,居然都是意外死亡膀捷,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門削彬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來全庸,“玉大人,你說我怎么就攤上這事融痛『” “怎么了?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵雁刷,是天一觀的道長拌消。 經(jīng)常有香客問我,道長安券,這世上最難降的妖魔是什么墩崩? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮侯勉,結(jié)果婚禮上鹦筹,老公的妹妹穿的比我還像新娘。我一直安慰自己址貌,他們只是感情好铐拐,可當(dāng)我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布徘键。 她就那樣靜靜地躺著,像睡著了一般遍蟋。 火紅的嫁衣襯著肌膚如雪吹害。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天虚青,我揣著相機與錄音它呀,去河邊找鬼。 笑死棒厘,一個胖子當(dāng)著我的面吹牛纵穿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播奢人,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼谓媒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了何乎?” 一聲冷哼從身側(cè)響起句惯,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎支救,沒想到半個月后抢野,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡搂妻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年蒙保,在試婚紗的時候發(fā)現(xiàn)自己被綠了辕棚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片欲主。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖逝嚎,靈堂內(nèi)的尸體忽然破棺而出扁瓢,到底是詐尸還是另有隱情,我是刑警寧澤补君,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布引几,位于F島的核電站,受9級特大地震影響挽铁,放射性物質(zhì)發(fā)生泄漏伟桅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一叽掘、第九天 我趴在偏房一處隱蔽的房頂上張望楣铁。 院中可真熱鬧,春花似錦更扁、人聲如沸盖腕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽溃列。三九已至劲厌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間听隐,已是汗流浹背补鼻。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留遵绰,地道東北人辽幌。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像椿访,于是被迫代替她去往敵國和親乌企。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容

  • 首先真的非常抱歉成玫,好久都沒有更新了加酵,這段時間在換工作,終于也算告一段落哭当,所以也空下來猪腕,有時間寫寫文字。有時候在想钦勘,...
    林帥并不帥閱讀 1,349評論 1 4
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,841評論 25 707
  • 背景: 閱讀新聞 11G ASM磁盤組不能自動MOUNT處理 [日期:2016-01-12] 來源:Linux社區(qū)...
    yanglei3849閱讀 3,827評論 0 2
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,778評論 6 342
  • 我從美國來到中國陋葡,感覺什么都很好奇,過去我們的媒體說的中國彻采,和我親眼看見的不太一樣腐缤。中國人很和善,很友好...
    銀河灣閱讀 121評論 0 0