Android | xml和view的那些事

嗨,我是寫博客滿腦子騷東西的哈利迪~今天和大伙聊聊Android中的xml和view的那些事舶衬,首先會分析一下xml布局解析inflate的流程住诸,然后會介紹一些業(yè)內(nèi)的方案维咸,如:

提效篇:

  • JakeWharton:著名的Butterknife
  • Android自帶:雙向綁定的DataBinding狐肢、省去findViewById的ViewBindingkotlin擴展添吗、

性能優(yōu)化篇:

  • 掌閱:將xml轉(zhuǎn)view的流程提前到編譯期的x2c
  • 鴻洋大佬最近研究的:自定義Factory來創(chuàng)建view的思路ViewOpt份名、
  • 天貓:把xml壓縮成二進制文件碟联,可動態(tài)下發(fā)、流式解析的VirtualView僵腺、

本文約5000字鲤孵,閱讀大約13分鐘。如個別大圖模糊辰如,可前往個人站點閱讀普监。

inflate

java層

源碼基于compileSdkVersion 29 和 androidx.appcompat:appcompat:1.1.0

通常,我們在開發(fā)布局的時候都是采用xml琉兜,這么做的好處一是可拖拽可預(yù)覽凯正,二是語法簡單清晰,然后在Activity中setContentView呕童,即可完成布局的加載漆际,那具體流程是怎么樣的呢?主要分為三步夺饲,io讀取xml文件奸汇,parser解析xml結(jié)構(gòu)得到view樹施符,反射創(chuàng)建view。我們從setContentView開始擂找,

//AppCompatActivity.java
void setContentView(int layoutResID) {
    //交給代理類處理
    getDelegate().setContentView(layoutResID);
}

//AppCompatDelegateImpl.java
void setContentView(int resId) {
    //默認(rèn)指定父布局為content
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
    //from通過系統(tǒng)服務(wù)得到服務(wù)對象
    //inflate解析布局戳吝,同時指定contentParent為父布局
    LayoutInflater.from(mContext).inflate(resId, contentParent);
}

可見,核心實現(xiàn)交給了LayoutInflater贯涎,跟進inflate方法听哭,

//LayoutInflater.java
View inflate(int resource, ViewGroup root, boolean attachToRoot) {
    //嘗試通過預(yù)編譯得到view,谷歌還在開發(fā)中的功能塘雳,先忽略
    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
    if (view != null) {
        return view;
    }
    //獲取XmlResourceParser
    XmlResourceParser parser = res.getLayout(resource);
    //開始解析
    return inflate(parser, root, attachToRoot);
}

XmlResourceParser是一個接口陆盘,實現(xiàn)了XmlPullParser(解析xml的布局結(jié)構(gòu))和 AttributeSet(解析xml標(biāo)簽屬性)兩個接口,我們先往下跟inflate败明,

//LayoutInflater.java
//inflate方法有一段注釋提到隘马,解析所用的是經(jīng)過預(yù)處理的xml二進制文件而非原始文件,這點后面分析
View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
    //得到xml里標(biāo)簽的屬性
    final AttributeSet attrs = Xml.asAttributeSet(parser);
    View result = root;
    //定位到view樹的根節(jié)點
    advanceToRootNode(parser);
    final String name = parser.getName();
    if (TAG_MERGE.equals(name)) {//根節(jié)點是merge標(biāo)簽
        if (root == null || !attachToRoot) {
            //merge標(biāo)簽必須指定父布局妻顶,否則拋異常
            throw new InflateException("<merge /> can be used only with a valid "
                                       + "ViewGroup root and attachToRoot=true");
        }
        //解析
        rInflate(parser, root, inflaterContext, attrs, false);
    } else {
        //得到根視圖
        final View temp = createViewFromTag(root, name, inflaterContext, attrs);
        ViewGroup.LayoutParams params = null;
        if (root != null) {
            //用傳入的contentParent父布局生成參數(shù)給根視圖
            params = root.generateLayoutParams(attrs);
            if (!attachToRoot) {
                temp.setLayoutParams(params);
            }
        }
        //解析
        rInflateChildren(parser, temp, attrs, true);
        if (root != null && attachToRoot) {
            //傳入的contentParent作為父布局
            root.addView(temp, params);
        }
        //沒有傳入父布局酸员,就直接返回根視圖
        if (root == null || !attachToRoot) {
            result = temp;
        }
    }
    return result;
}

繼續(xù)跟進rInflateChildren

//LayoutInflater.java
void rInflateChildren(...){
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

//遍歷讳嘱、遞歸(比如LinearLayout里又有一個LinearLayout)
void rInflate(XmlPullParser parser, View parent, Context context,
              AttributeSet attrs, boolean finishInflate){
    //view樹深度
    final int depth = parser.getDepth();
    int type;
    boolean pendingRequestFocus = false;
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
        if (type != XmlPullParser.START_TAG) {
            continue;
        }
        final String name = parser.getName();
        //根據(jù)不同的標(biāo)簽名進行各自的操作
        if (TAG_REQUEST_FOCUS.equals(name)) {//requestFocus
            pendingRequestFocus = true;
            consumeChildElements(parser);
        } else if (TAG_TAG.equals(name)) {//tag
            //...
        } else if (TAG_MERGE.equals(name)) {//merge
            throw new InflateException("<merge /> must be the root element");
        } else {//重點關(guān)注
            //創(chuàng)建view
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            //遞歸
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }
    //...
}

跟進createViewFromTag幔嗦,

//LayoutInflater.java
View createViewFromTag(View parent, String name, Context context, 
                       AttributeSet attrs,boolean ignoreThemeAttr) {
    if (name.equals("view")) {
        //如果是小寫的view,就取class屬性的值作為名字
        name = attrs.getAttributeValue(null, "class");
    }
    //選擇一個工廠來創(chuàng)建view沥潭,可以setFactory/setFactory2來自定義工廠邀泉,干預(yù)view的創(chuàng)建
    View view = tryCreateView(parent, name, context, attrs);
    if (view == null) {//工廠處理不了的view,就手動創(chuàng)建
        if (-1 == name.indexOf('.')) {
            //像<Button/>這樣沒有包名钝鸽,則加上前綴android.view.
            //運行時呼渣,真正的實例是子類PhoneLayoutInflater,他會先在3個前綴里選一個:
            //android.widget.  android.webkit.  android.app.
            //如果3個前綴都找不到類寞埠,才交給父類使用前綴android.view.
            view = onCreateView(context, parent, name, attrs);
        } else {
            //已有包名
            view = createView(context, name, null, attrs);
        }
    }
    return view;
}

跟進createView屁置,

//LayoutInflater.java
//通過反射創(chuàng)建view
View createView(Context viewContext, String name,String prefix, AttributeSet attrs){
    //從緩存中取出構(gòu)造方法
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    Class<? extends View> clazz = null;
    //加載class
    clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                          mContext.getClassLoader()).asSubclass(View.class);
    //緩存里沒有構(gòu)造方法,則用clazz獲取兩個參數(shù)的構(gòu)造方法
    if (constructor == null) {
        //Class<?>[] mConstructorSignature = new Class[] {Context.class, AttributeSet.class}
        constructor = clazz.getConstructor(mConstructorSignature);
    }
    //...
    //反射創(chuàng)建view
    View view = constructor.newInstance(args);
    return view;
}

以上就是常規(guī)流程仁连,如果有設(shè)置工廠蓝角,則可以在tryCreateView中就把view給創(chuàng)建了。利用工廠可以做一些全局處理饭冬,比如一鍵切換皮膚使鹅、字體等,

//LayoutInflater.java
View tryCreateView(View parent, String name,Context context,AttributeSet attrs) {
    View view;
    //選擇一個工廠來創(chuàng)建view昌抠,可以setFactory/setFactory2來自定義工廠患朱,干預(yù)view的創(chuàng)建
    if (mFactory2 != null) {
        //用工廠創(chuàng)建view
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        //用工廠創(chuàng)建view
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }
    if (view == null && mPrivateFactory != null) {
        //用工廠創(chuàng)建view
        view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }
    return view;
}

整體流程圖如下,

image

需要注意的是炊苫,目前系統(tǒng)的AppCompatActivity有幫我們設(shè)置一個默認(rèn)工廠裁厅,

AppCompatActivity#onCreate ->

? delegate.installViewFactory();

AppCompatDelegateImpl#installViewFactory ->

? LayoutInflaterCompat.setFactory2(layoutInflater, this);

AppCompatDelegateImpl中冰沙,

AppCompatDelegateImpl#createView ->

? return mAppCompatViewInflater.createView(...);

AppCompatViewInflater中可見,我們常見的一些view都被轉(zhuǎn)換成AppCompat的view了执虹,他們的創(chuàng)建不需要走反射邏輯拓挥。

//AppCompatViewInflater.java
View createView(...) {
    switch (name) {
        case "TextView":
            view = createTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "ImageView":
            view = createImageView(context, attrs);
            verifyNotNull(view, name);
            break;
            //...
    }
    return view;
}

protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
    //TextView被替換成AppCompatTextView
    return new AppCompatTextView(context, attrs);
}

native層

那么java層的parser的具體實例是誰呢?跟進XmlResourceParser parser = res.getLayout(resource)袋励,最終發(fā)現(xiàn)是XmlBlock.Parser侥啤,我們試著跟下parsergetName方法,他的實現(xiàn)交給了native層的nativeGetName茬故,

native源碼基于Android 9.0

native函數(shù)動態(tài)注冊盖灸,android_util_XmlBlock.cpp

//android_util_XmlBlock.cpp

//需要動態(tài)注冊的native函數(shù)數(shù)組
static const JNINativeMethod gXmlBlockMethods[] = {
    { "nativeGetName","(J)I",(void*) android_content_XmlBlock_nativeGetName }
    //...
}

static jint android_content_XmlBlock_nativeGetName(JNIEnv* env, jobject clazz,
                                                   jlong token){
    ResXMLParser* st = reinterpret_cast<ResXMLParser*>(token);
    if (st == NULL) {
        return -1;
    }
    return static_cast<jint>(st->getElementNameID());
}

來看到ResXMLParsergetElementNameID方法,ResourceTypes.cpp

//ResourceTypes.cpp
int32_t ResXMLParser::getElementNameID() const{
    if (mEventCode == START_TAG) {//標(biāo)簽開始處磺芭,如<View>
        //dtohl是啥糠雨?todo1
        return dtohl(((const ResXMLTree_attrExt*)mCurExt)->name.index);
    }
    if (mEventCode == END_TAG) {//標(biāo)簽結(jié)束處,如</View>
        return dtohl(((const ResXMLTree_endElementExt*)mCurExt)->name.index);
    }
    return -1;
}

先看下ResXMLTree_attrExt是啥徘跪,在ResourceTypes.h

//ResourceTypes.h
//是一個結(jié)構(gòu)體
struct ResXMLTree_attrExt
{
    //當(dāng)前標(biāo)簽元素的命名空間
    struct ResStringPool_ref ns;
    //當(dāng)前標(biāo)簽元素的名稱,如"View"琅攘,但并不是字符串類型垮庐,而是一個結(jié)構(gòu)體,往下看
    struct ResStringPool_ref name;
    //...
};

//結(jié)構(gòu)體坞琴,有個int字段哨查,表示在字符串常量池中的索引
struct ResStringPool_ref
{
    //從ResStringPool_header(頭部標(biāo)識)之后開始索引,在該表中查找字符串在池子中的位置
    uint32_t index;
};

可見剧辐,xml被二進制處理時寒亥,會把多個相同的字符串壓縮成一份存進常量池里,如:

image

根據(jù)位置index字段荧关,就可以知道標(biāo)簽名字是啥了溉奕,常量池的處理可以減小xml體積,

文章前邊留了個todo1:dtohl是啥忍啤,谷歌一下dtohl加勤,發(fā)現(xiàn)這些函數(shù)被定義在ByteOrder.h里,

//ByteOrder.h
//跟設(shè)備架構(gòu)有關(guān)的字節(jié)序同波,對于我們今天使用的ARM CPU鳄梅,就是小字節(jié)序(不太懂)
#define DEVICE_BYTE_ORDER LITTLE_ENDIAN
#if BYTE_ORDER == DEVICE_BYTE_ORDER  //不用進行字節(jié)轉(zhuǎn)換,傳x直接返回x
#define dtohl(x)    (x)
#define dtohs(x)    (x)
#define htodl(x)    (x)
#define htods(x)    (x)
#else  //需要進行字節(jié)轉(zhuǎn)換
#define dtohl(x)    (android_swap_long(x))
#define dtohs(x)    (android_swap_short(x))
#define htodl(x)    (android_swap_long(x))
#define htods(x)    (android_swap_short(x))
#endif

//轉(zhuǎn)換操作未檩,不太懂是拿來做啥的
static inline uint32_t android_swap_long(uint32_t v)
{
    return (v<<24) | ((v<<8)&0x00FF0000) | ((v>>8)&0x0000FF00) | (v>>24);
}

static inline uint16_t android_swap_short(uint16_t v)
{
    return (v<<8) | (v>>8);
}

哈迪能力有限戴尸,只能跟到這里了。我們知道運行時解析的xml是經(jīng)過預(yù)處理的二進制文件(apk打包時做的)冤狡,那我們可以大膽猜測一下孙蒙,運行時的解析是不是在做一些流式项棠、指針移位之類的讀操作?比如马篮,把xml二進制文件進行各種分區(qū)沾乘,如文件頭、標(biāo)簽區(qū)浑测、屬性區(qū)翅阵、字符串常量池區(qū),然后解析時則用如readShort迁央、readLong之類的方式進行指針移位掷匠,從而讀出相應(yīng)的view標(biāo)簽、view屬性岖圈,有點類似JVM解析字節(jié)碼的過程讹语。(能力有限,僅做猜測)

image

小結(jié)

  1. 預(yù)編譯tryInflatePrecompiled:谷歌正在做的事情蜂科,還沒開放顽决,敬請期待。
  2. xml文件的預(yù)處理:打包時將xml進行二進制編譯导匣,壓縮xml體積才菠、提升運行時的解析效率。(猜測:二進制的流式贡定、指針移位操作赋访,解析效率要比原始的xml高)

Butterknife

Butterknife在編譯期通過Apt(注解處理器)處理注解,JavaPoet(輔助生成Java文件的工具)創(chuàng)建類缓待,來省去findViewById蚓耽、setOnclickListener這些繁瑣的操作。哈迪使用時還是在大學(xué)的時候旋炒,工作后也沒接觸過了步悠,現(xiàn)在這個項目的作者已經(jīng)不再維護了,他推薦我們?nèi)ナ褂?code>ViewBinding瘫镇,不過我們還是簡單回顧下吧~

引入依賴:

implementation 'com.jakewharton:butterknife:10.2.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'

簡單使用:

class ButterknifeActivity extends AppCompatActivity {
    @BindView(R.id.tv_name)
    TextView mTextView;
    @OnClick(R.id.tv_name)
    void submit() {
        Toast.makeText(this, "click", Toast.LENGTH_SHORT).show();
    }

    void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_butterknife);
        //綁定
        mUnbinder = ButterKnife.bind(this);
        //直接訪問
        mTextView.setText("butter knife");
    }
}

跟進bind方法贤徒,

//ButterKnife.java
static Unbinder bind(Activity target) {
    //獲取DecorView
    View sourceView = target.getWindow().getDecorView();
    return bind(target, sourceView);
}

static Unbinder bind(Object target, View source) {
    Class<?> targetClass = target.getClass();
    //獲取構(gòu)造方法
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
    if (constructor == null) {
        return Unbinder.EMPTY;
    }
    //反射創(chuàng)建Unbinder的實例
    return constructor.newInstance(target, source);
}

跟進findBindingConstructorForClass

static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    //從緩存獲取構(gòu)造方法
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null || BINDINGS.containsKey(cls)) {
        return bindingCtor;
    }
    String clsName = cls.getName();
    //不支持framework層的類
    if (clsName.startsWith("android.") || clsName.startsWith("java.")
        || clsName.startsWith("androidx.")) {
        return null;
    }
    //加載ButterknifeActivity_ViewBinding類
    Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
    //獲取兩個參數(shù)的構(gòu)造方法
    bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
    //把構(gòu)造方法存入緩存
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
}

ButterknifeActivity_ViewBinding類是由Butterknife創(chuàng)建的汇四,代碼不多接奈,

class ButterknifeActivity_ViewBinding implements Unbinder {
    private ButterknifeActivity target;
    private View view7f0700b7;

    @UiThread
    public ButterknifeActivity_ViewBinding(final ButterknifeActivity target, View source) {
        this.target = target;
        View view;
        //source是DecorView,這里邊就是簡單的source.findViewById(id)
        view = Utils.findRequiredView(source, R.id.tv_name, "field 'mTextView' and method 'submit'");
        //強轉(zhuǎn)通孽,并賦值給ButterknifeActivity的mTextView
        //所以mTextView不能是private的序宦,private意味著需要增加反射來實現(xiàn),影響性能
        target.mTextView = Utils.castView(view, R.id.tv_name, "field 'mTextView'", TextView.class);
        view7f0700b7 = view;
        //設(shè)置點擊事件
        view.setOnClickListener(new DebouncingOnClickListener() {
            @Override
            public void doClick(View p0) {
                //調(diào)用ButterknifeActivity里的submit方法
                target.submit();
            }
        });
    }

    public void unbind() {
        //解綁時的一些清理邏輯
        this.target = null;
        target.mTextView = null;
        view7f0700b7.setOnClickListener(null);
        view7f0700b7 = null;
    }
}

可見背苦,Butterknife只有在創(chuàng)建Unbinder實例的時候用了反射互捌,所以對運行時性能的影響是不大的潘明。Apt處理注解和創(chuàng)建類的常規(guī)流程就不分析了哈~

優(yōu)勢:

  1. 省去findViewById、setOnclickListener這些繁瑣的操作
  2. 反射操作很少秕噪,對運行時性能影響不大

缺點:

  1. apt創(chuàng)建類钳降,增加io耗時,類編譯耗時
  2. 類的增多腌巾,意味著包體積增大

DataBinding/ViewBinding/kotlin擴展

DataBinding

DataBinding可以通過binding對象直接訪問到xml布局里的有id控件遂填,而且他還能實現(xiàn)數(shù)據(jù)和UI的雙向綁定,即數(shù)據(jù)驅(qū)動UI刷新澈蝙,UI操作修改數(shù)據(jù)吓坚,雙向綁定不是本文重點,本文主要討論xml和view的事兒~

簡單使用:

// app/build.gradle里android{}加上開關(guān)
dataBinding {
    enabled = true
}

xml布局轉(zhuǎn)成data binding layout灯荧,也就是在布局外層包一層layout標(biāo)簽礁击,然后多出一個data標(biāo)簽表示數(shù)據(jù)區(qū),

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">
    <data>
    </data>

    <LinearLayout>

        <TextView
                  android:id="@+id/tv_name"/>
    </LinearLayout>
</layout>

在activity中逗载,通過DataBindingUtil得到binding對象哆窿,

class DBActivity extends AppCompatActivity {

    void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //得到binding對象
        ActivityDBBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_d_b);
        //直接訪問控件
        binding.tvName.setText("data binding");
    }
}

DataBinding是怎么做到的呢?也是通過生成額外的一些類來實現(xiàn)的厉斟,感興趣可以看下哈迪之前寫的筆記-DataBinding挚躯,我們直接看生成的類app/build/generated/data_binding_base_class_source_out/debug/out/com/holiday/srccodestudy/databinding/ActivityDBBinding.java

abstract class ActivityDBBinding extends ViewDataBinding {
    //public的TextView捏膨,可以直接訪問
    public final TextView tvName;

    protected ActivityDBBinding(Object _bindingComponent, View _root, 
                                int _localFieldCount,TextView tvName) {
        super(_bindingComponent, _root, _localFieldCount);
        this.tvName = tvName;
    }

    //省略一些inflate、bind方法
}

ViewBinding

ViewBinding省去了雙向綁定的邏輯食侮,比DataBinding更輕量号涯,用法差不多,不過需要Android studio 3.6開始才能使用嚼黔,

// app/build.gradle里android{}加上開關(guān)
viewBinding {
    enabled = true
}

打開開關(guān)后租幕,默認(rèn)會給所有布局生成java類凡辱,不像DataBinding需要包上一層layout標(biāo)簽。如果個別布局不需要開啟ViewBinding域蜗,可以給布局的根標(biāo)簽加上tools:viewBindingIgnore="true"

在activity中使用噪猾,有點不同霉祸,

class VBActivity extends AppCompatActivity {

    void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //先inflate
        ActivityVBBinding binding = ActivityVBBinding.inflate(LayoutInflater.from(this));
        //getRoot獲取rootView
        setContentView(binding.getRoot());
        //直接訪問控件
        binding.tvVb.setText("view binding");
    }
}

ViewBinding的具體實現(xiàn)暫不關(guān)注,直接看他的生成類app/build/generated/data_binding_base_class_source_out/debug/out/com/holiday/srccodestudy/databinding/ActivityVBBinding.java袱蜡,路徑跟DataBinding一樣的丝蹭,

final class ActivityVBBinding implements ViewBinding {
    private final LinearLayout rootView;
    //public的TextView,可以直接訪問
    public final TextView tvVb;

    private ActivityVBBinding(LinearLayout rootView, TextView tvVb) {
        this.rootView = rootView;
        this.tvVb = tvVb;
    }

    //比DataBinding多出一個getRoot方法
    public LinearLayout getRoot() {
        return rootView;
    }

    //省略一些inflate坪蚁、bind方法
}

ViewBinding省去了DataBinding雙向綁定功能(不用處理DataBinding的注解奔穿、表達式等)镜沽,更專注于解決findViewById的問題,所以更輕量贱田,編譯更快缅茉。

kotlin擴展

如果項目有使用kotlin,還可以使用kotlin的擴展插件來免去findViewById操作男摧。

使用kotlin擴展插件蔬墩,

// app/build.gradle
apply plugin: 'kotlin-android-extensions'

在activity中使用,

class KotlinActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_kotlin)
        //直接訪問控件
        tv_kotlin.text = "kotlin 擴展插件"
    }
}

使用kotlin擴展插件有個明顯的問題彩倚,就是控件的“裸奔”問題筹我,比如我在activity中輸入tv,就會把其他頁面的控件也提示出來帆离,

image

如果不小心導(dǎo)入了別的頁面才有的控件蔬蕊,編譯期沒問題,運行的時候就才拋異常哥谷。也就是說岸夯,使用kotlin擴展插件,所有控件都處于不安全的裸奔狀態(tài)们妥。

使用AS反編譯一下KotlinActivity猜扮,Tools -> Kotlin -> Show Kotlin Bytecode -> Decompile

final class KotlinActivity extends AppCompatActivity {
    private HashMap _$_findViewCache;//控件緩存

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(1300004);
        //查找TextView
        TextView var10000 = (TextView)this._$_findCachedViewById(id.tv_kotlin);
        //運行時监婶,合法性檢測旅赢,如果導(dǎo)了別的頁面的控件進來,會拋xxx must not be null異常
        Intrinsics.checkExpressionValueIsNotNull(var10000, "tv_kotlin");
        //使用控件
        var10000.setText((CharSequence)"kotlin 擴展插件");
    }

    public View _$_findCachedViewById(int var1) {
        if (this._$_findViewCache == null) {
            this._$_findViewCache = new HashMap();
        }
        //在緩存中找控件
        View var2 = (View)this._$_findViewCache.get(var1);
        if (var2 == null) {
            //第一次找不到惑惶,走findViewById(如果導(dǎo)了別的頁面的控件進來煮盼,就只能返回null了)
            var2 = this.findViewById(var1);
            this._$_findViewCache.put(var1, var2);
        }
        return var2;
    }
}

至于kotlin如何插入這些代碼的,能力有限带污,哈迪也不知道僵控,有了解的朋友評論區(qū)聊起來~

小結(jié)

如果不做數(shù)據(jù)和UI的雙向綁定,只是為了避免findViewById鱼冀,優(yōu)先使用更輕量的ViewBinding报破,否則使用DataBindingDataBindingViewBinding在避免了findViewById繁瑣工作的同時千绪,還確保了空安全類型安全充易,即不會出現(xiàn)findViewById得到null、view cast exception的問題荸型。當(dāng)然蔽氨,這兩種方式也是避免不了生成類的編譯耗時和包體積增大的問題的,得結(jié)合具體場景來使用。至于kotlin擴展鹉究,存在控件裸奔問題宇立,不太推薦。

至此自赔,提效篇就介紹到這里了妈嘹,下面讓我們開始性能優(yōu)化篇~

x2c

x2c是使用Apt+JavaPoet技術(shù),在編譯期將xml布局轉(zhuǎn)成view類绍妨,免去了運行時解析xml的耗時润脸。

引入依賴:

annotationProcessor 'com.zhangyue.we:x2c-apt:1.1.2'
implementation 'com.zhangyue.we:x2c-lib:1.0.6'

簡單使用:

//給布局文件聲明一個注解
@Xml(layouts = "layout_x2c_test")
class X2CActivity extends AppCompatActivity {

    void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_x2c);
        //通過X2C來獲取view
        View view = X2C.inflate(this, R.layout.layout_x2c_test, null);
    }
}

跟進X2C.inflate

//X2C.java

static View inflate(Context context, int layoutId, ViewGroup parent) {
    return inflate(context, layoutId, parent, true);
}

//加載xml文件他去,檢測如果有對應(yīng)的java類毙驯,使用java類,否則使用LayoutInflater
static View inflate(Context context, int layoutId, ViewGroup parent, boolean attach) {
    //獲取xml布局對應(yīng)的java類
    View view = getView(context, layoutId);
    if (view != null) {
        if (parent != null) {
            parent.addView(view);
        }
        return view;
    } else {
        //沒有java類灾测,走LayoutInflater解析邏輯
        return LayoutInflater.from(context).inflate(layoutId, parent, attach);
    }
}

static View getView(Context context, int layoutId) {
    //獲取緩存好的view創(chuàng)建器
    IViewCreator creator = sSparseArray.get(layoutId);
    if (creator == null) {
        //沒有爆价,則生成一個
        int group = generateGroupId(layoutId);
        String layoutName = context.getResources().getResourceName(layoutId);
        layoutName = layoutName.substring(layoutName.lastIndexOf("/") + 1);
        String clzName = "com.zhangyue.we.x2c.X2C" + group + "_" + layoutName;
        //加載Apt生成的X2C0_layout_x2c_test這個類,反射獲得view創(chuàng)建器
        creator = (IViewCreator) context.getClassLoader().loadClass(clzName).newInstance();
        //緩存起來
        sSparseArray.put(layoutId, creator);
    }
    //使用view創(chuàng)建器來創(chuàng)建view
    return creator.createView(context);
}

來看到view創(chuàng)建器X2C0_layout_x2c_test媳搪,

class X2C0_layout_x2c_test implements IViewCreator {

    View createView(Context context) {
        return new com.zhangyue.we.x2c.layouts.X2C0_Layout_X2c_Test().createView(context);
    }
}

跟進X2C0_Layout_X2c_Test可見铭段,xml的標(biāo)簽和屬性,都被解析成了java類的相應(yīng)設(shè)置秦爆,

class X2C0_Layout_X2c_Test implements IViewCreator {

    View createView(Context ctx) {
        Resources res = ctx.getResources();
        LinearLayout linearLayout0 = new LinearLayout(ctx);
        linearLayout0.setGravity(Gravity.CENTER_HORIZONTAL);
        linearLayout0.setOrientation(LinearLayout.VERTICAL);
        //...
        TextView textView2 = new TextView(ctx);
        LinearLayout.LayoutParams layoutParam2 = new LinearLayout.LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
        textView2.setId(R.id.tv);
        textView2.setText("文案內(nèi)容");
        textView2.setLayoutParams(layoutParam2);
        linearLayout0.addView(textView2);
        return linearLayout0;
    }
}

優(yōu)勢:

  1. 將xml解析提前到編譯期序愚,免去了運行時解析的耗時和內(nèi)存
  2. 只在獲取view創(chuàng)建器時用了反射,對運行時性能影響不大

缺點:

  1. apt創(chuàng)建類等限,增加io耗時爸吮,類編譯耗時
  2. 類的增多,意味著包體積增大

所以望门,通常只在個別復(fù)雜度較高形娇,有性能瓶頸的頁面才會使用。

ViewOpt

鴻洋大佬的方案怒允,是從避免反射創(chuàng)建view的角度去做優(yōu)化的埂软,即使用自定義工廠Factory來創(chuàng)建view锈遥,繞開反射邏輯纫事。核心流程就是,先通過merge.xml來收集xml中用到的view集合所灸,然后Apt生成一個類來處理集合丽惶,然后干預(yù)默認(rèn)工廠Factory來插入自己的view創(chuàng)建邏輯。

image
class BaseActivity extends AppCompatActivity {

    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        //插入自己的邏輯爬立,將view的創(chuàng)建交給代理類
        View view = ViewOpt.createView(name, context, attrs);
        if (view != null) {
            return view;
        }
        //走默認(rèn)工廠
        return super.onCreateView(parent, name, context, attrs);
    }
}

更多細(xì)節(jié)钾唬,可前往Android“退一步”的布局加載優(yōu)化閱讀~

延伸:VirtualView

VirtualView是在天貓重運營的電商業(yè)務(wù)場景下,產(chǎn)生的一套方案,他可以通過編寫xml抡秆,然后編譯成二進制文件(體積小奕巍,解析快),下發(fā)到客戶端渲染儒士,具備動態(tài)能力的止。感興趣可以看哈迪之前寫的系列文章硬核的Virtualview

哈迪在inflate章節(jié)中猜測:Android中的xml的二進制解析是不是流式着撩、指針移位的方式來操作诅福?之所以這么想,是因為在VirtualView文件格式與模板編譯這篇文章看到了類似操作拖叙,所以做出了這個猜測氓润。

總結(jié)

不管是提效篇還是性能優(yōu)化篇,我們可以看到薯鳍,針對不同的業(yè)務(wù)場景和需求咖气,來選擇不同的實現(xiàn)方案。沒有完美的技術(shù)辐啄,只有合不合適~

參考資料


image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末采章,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子壶辜,更是在濱河造成了極大的恐慌悯舟,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件砸民,死亡現(xiàn)場離奇詭異抵怎,居然都是意外死亡,警方通過查閱死者的電腦和手機岭参,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門反惕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人演侯,你說我怎么就攤上這事姿染。” “怎么了秒际?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵悬赏,是天一觀的道長。 經(jīng)常有香客問我娄徊,道長闽颇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任寄锐,我火速辦了婚禮兵多,結(jié)果婚禮上尖啡,老公的妹妹穿的比我還像新娘。我一直安慰自己剩膘,他們只是感情好衅斩,可當(dāng)我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著怠褐,像睡著了一般矛渴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上惫搏,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天具温,我揣著相機與錄音,去河邊找鬼筐赔。 笑死铣猩,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的茴丰。 我是一名探鬼主播达皿,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼贿肩!你這毒婦竟也來了峦椰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤汰规,失蹤者是張志新(化名)和其女友劉穎汤功,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體溜哮,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡滔金,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了茂嗓。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片餐茵。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖述吸,靈堂內(nèi)的尸體忽然破棺而出忿族,到底是詐尸還是另有隱情,我是刑警寧澤蝌矛,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布道批,位于F島的核電站,受9級特大地震影響朴读,放射性物質(zhì)發(fā)生泄漏屹徘。R本人自食惡果不足惜走趋,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一衅金、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦氮唯、人聲如沸鉴吹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽豆励。三九已至,卻和暖如春瞒渠,著一層夾襖步出監(jiān)牢的瞬間良蒸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工伍玖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嫩痰,地道東北人。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓窍箍,卻偏偏與公主長得像串纺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子椰棘,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,486評論 2 348