0xA03 Android 10 源碼分析:Apk加載流程之資源加載

引言

  • 這是 Android 10 源碼分析系列的第 3 篇
  • 分支:android-10.0.0_r14
  • 全文閱讀大概 15 分鐘

經過這篇文章你將學習到如下內容,文末會給出相應的答案java

  • LayoutInflater的inflate方法的三個參數都表明什麼意思?
  • 系統對merge、include是如何處理的
  • merge標籤爲何能夠起到優化佈局的效果?
  • xml中的view是如何被實例化的?
  • 爲何複雜佈局會產生卡頓?在Android 10上作了那些優化?
  • BlinkLayout是什麼?

前面兩篇文章0xA01 Android 10 源碼分析:Apk是如何生成的0xA02 Android 10 源碼分析:Apk的安裝流程分析了Apk大概能夠分爲代碼和資源兩部分,那麼Apk的加載也是分爲代碼和資源兩部分,代碼的加載涉及了進程的建立、啓動、調度,本文主要來分析一下資源的加載,若是沒有看過 Apk是如何生成的Apk的安裝流程 能夠點擊下方鏈接前往:android

1. Android資源

Android資源大概分爲兩個部分:assets 和 resgit

assets資源github

assets資源放在assets目錄下,它裏面保存一些原始的文件,能夠以任何方式來進行組織,這些文件最終會原封不動的被打包進APK文件中,經過AssetManager來獲取asset資源,代碼以下web

AssetManager assetManager = context.getAssets();
InputStream is = assetManager.open("fileName");
複製代碼

res資源算法

res資源放在主工程的res目錄下,這類資源通常都會在編譯階段生成一個資源ID供咱們使用,res目錄包括animator、anim、 color、drawable、layout、menu、raw、values、xml等,經過getResource()去獲取Resources對象編程

Resources res = getContext().getResources();
複製代碼

Apk的生成過程當中,會生成資源索引表resources.arsc文件和R.java文件,前者資源索引表resources.arsc記錄了全部的應用程序資源目錄的信息,包括每個資源名稱、類型、值、ID以及所配置的維度信息,後者定義了各個資源ID常量,運行時經過Resources和 AssetManger共同完成資源的加載,若是資源是個文件,Resouces先根據資源id查找出文件名,AssetManger再根據文件名查找出具體的資源,關於resources.arsc,能夠查看0xA01 ASOP應用框架:Apk是如何生成的
canvas

2. 資源的加載和解析到View的生成

下面代碼必定不會很陌生,在Activity常見的幾行代碼緩存

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main_activity)
}
複製代碼

一塊兒來分析一下調用setContentView方法以後作了什麼事情,接下來查看一下Activity中的setContentView方法 frameworks/base/core/java/android/app/Activity.javabash

public void setContentView(@LayoutRes int layoutResID) {
    // 實際上調用的是PhoneWindow.setContentView方法
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}
複製代碼

調用getWindow方法返回的是mWindow,mWindow是Windowd對象,其實是調用它的惟一實現類PhoneWindow.setContentView方法

2.1 Activity -> PhoneWindow

PhoneWindow 是Window的惟一實現類,它的結構以下:

當調用Activity.setContentView方法實際上調用的是PhoneWindow.setContentView方法 frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java

public void setContentView(int layoutResID) {
    // mContentParent是id爲ID_ANDROID_CONTENT的FrameLayout
    // 調用setContentView方法,就是給id爲ID_ANDROID_CONTENT的view添加子view
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        // FEATURE_CONTENT_TRANSITIONS,則是標記當前內容加載有沒有使用過分動畫
        // 若是內容已經加載過,而且不須要動畫,則會調用removeAllViews
        mContentParent.removeAllViews();
    }

    // 檢查是否設置了FEATURE_CONTENT_TRANSITIONS
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        // 解析指定的xml資源文件
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}
複製代碼
  • 先判斷mContentParent是否爲空,若是爲空則調用installDecor方法,生成mDecor,並將它賦值給mContentParent
  • 根據FEATURE_CONTENT_TRANSITIONS標記來判斷是否加載過轉場動畫
  • 若是設置了FEATURE_CONTENT_TRANSITIONS則添加Scene來過分啓動,不然調用mLayoutInflater.inflate(layoutResID, mContentParent),解析資源文件,建立view, 並添加到mContentParent視圖中

2.2 PhoneWindow -> LayoutInflater

當調用PhoneWindow.setContentView方法,以後調用LayoutInflater.inflate方法,來解析xml資源文件 frameworks/base/core/java/android/view/LayoutInflater.java

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(resource, root, root != null);
}
複製代碼

inflate它有多個重載方法,最後調用的是inflate(resource, root, root != null)方法 frameworks/base/core/java/android/view/LayoutInflater.java

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    // 根據xml預編譯生成compiled_view.dex, 而後經過反射來生成對應的View,從而減小XmlPullParser解析Xml的時間
    // 須要注意的是在目前的release版本中不支持使用
    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
    if (view != null) {
        return view;
    }
    // 獲取資源解析器 XmlResourceParser
    XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}
複製代碼

這個方法主要作了三件事:

  • 根據xml預編譯生成compiled_view.dex, 而後經過反射來生成對應的View
  • 獲取XmlResourceParser
  • 解析view

注意:在目前的release版本中不支持使用tryInflatePrecompiled方法源碼以下:

private void initPrecompiledViews() {
    // Precompiled layouts are not supported in this release.
    // enabled 是否啓動預編譯佈局,這裏始終爲false
    boolean enabled = false;
    initPrecompiledViews(enabled);
}

private void initPrecompiledViews(boolean enablePrecompiledViews) {
    mUseCompiledView = enablePrecompiledViews;

    if (!mUseCompiledView) {
        mPrecompiledClassLoader = null;
        return;
    }

    ...
}

View tryInflatePrecompiled(@LayoutRes int resource, Resources res, @Nullable ViewGroup root,
    boolean attachToRoot) {
    // mUseCompiledView始終爲false
    if (!mUseCompiledView) {
        return null;
    }

    // 獲取須要解析的資源文件的 pkg 和 layout
    String pkg = res.getResourcePackageName(resource);
    String layout = res.getResourceEntryName(resource);

    try {
        // 根據mPrecompiledClassLoader經過反射獲取預編譯生成的view對象的Class類
        Class clazz = Class.forName("" + pkg + ".CompiledView", false, mPrecompiledClassLoader);
        Method inflater = clazz.getMethod(layout, Context.class, int.class);
        View view = (View) inflater.invoke(null, mContext, resource);

        if (view != null && root != null) {
            // 將生成的view 添加根佈局中
            XmlResourceParser parser = res.getLayout(resource);
            try {
                AttributeSet attrs = Xml.asAttributeSet(parser);
                advanceToRootNode(parser);
                ViewGroup.LayoutParams params = root.generateLayoutParams(attrs);
                // 若是 attachToRoot=true添加到根佈局中
                if (attachToRoot) {
                    root.addView(view, params);
                } else {
                    // 否者將獲取到的根佈局的LayoutParams,設置到生成的view中
                    view.setLayoutParams(params);
                }
            } finally {
                parser.close();
            }
        }

        return view;
    } catch (Throwable e) {
        
    } finally {
    }
    return null;
}
複製代碼
  • tryInflatePrecompiled方法是Android 10 新增的方法,這是一個在編譯器運行的一個優化,由於佈局文件越複雜XmlPullParser解析Xml越耗時, tryInflatePrecompiled方法根據xml預編譯生成compiled_view.dex, 而後經過反射來生成對應的View,從而減小XmlPullParser解析Xml的時間,而後根據attachToRoot參數來判斷是添加到根佈局中,仍是設置LayoutParams參數返回給調用者
  • 用一個全局變量mUseCompiledView來控制是否啓用tryInflatePrecompiled方法,根據源碼分析,mUseCompiledView始終爲false

瞭解了tryInflatePrecompiled方法以後,在來查看一下inflate方法中的三個參數都什麼意思

  • resource:要解析的xml佈局文件Id
  • root:表示根佈局
  • attachToRoot:是否要添加到父佈局root中

resource其實很好理解就是資源Id,而root 和 attachToRoot 分別表明什麼意思:

  • 當attachToRoot == true且root != null時,新解析出來的View會被add到root中去,而後將root做爲結果返回
  • 當attachToRoot == false且root != null時,新解析的View會直接做爲結果返回,並且root會爲新解析的View生成LayoutParams並設置到該View中去
  • 當attachToRoot == false且root == null時,新解析的View會直接做爲結果返回

根據源碼知道調用tryInflatePrecompiled方法返回的view爲空,繼續往下執行調用Resources的getLayout方法獲取資源解析器 XmlResourceParser

2.3 LayoutInflater -> Resources

上面說到XmlResourceParser是經過調用Resources的getLayout方法獲取的,getLayout方法又去調用了Resources的loadXmlResourceParser方法 frameworks/base/core/java/android/content/res/Resources.java

public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
    return loadXmlResourceParser(id, "layout");
}

XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
        throws NotFoundException {
    // TypedValue 主要用來存儲資源
    final TypedValue value = obtainTempTypedValue();
    try {
        final ResourcesImpl impl = mResourcesImpl;
        // 獲取xml資源,保存到 TypedValue
        impl.getValue(id, value, true);
        if (value.type == TypedValue.TYPE_STRING) {
            // 爲指定的xml資源,加載解析器
            return impl.loadXmlResourceParser(value.string.toString(), id,
                    value.assetCookie, type);
        }
        throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
                + " type #0x" + Integer.toHexString(value.type) + " is not valid");
    } finally {
        releaseTempTypedValue(value);
    }
}
複製代碼

TypedValue是動態的數據容器,主要用來存儲Resource的資源,獲取xml資源保存到 TypedValue,以後調用 ResourcesImpl 的loadXmlResourceParser方法加載對應的解析器

2.4 Resources -> ResourcesImpl

ResourcesImpl實現了Resource的訪問,它包含了AssetManager和全部的緩存,經過Resource的getValue方法獲取xml資源保存到 TypedValue,以後就會調用ResourcesImpl的loadXmlResourceParser方法對該佈局資源進行解析 frameworks/base/core/java/android/content/res/ResourcesImpl.java

XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,
        @NonNull String type)
        throws NotFoundException {
    if (id != 0) {
        try {
            synchronized (mCachedXmlBlocks) {
                final int[] cachedXmlBlockCookies = mCachedXmlBlockCookies;
                final String[] cachedXmlBlockFiles = mCachedXmlBlockFiles;
                final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks;
                // 首先從緩存中查找xml資源
                final int num = cachedXmlBlockFiles.length;
                for (int i = 0; i < num; i++) {
                    if (cachedXmlBlockCookies[i] == assetCookie && cachedXmlBlockFiles[i] != null
                            && cachedXmlBlockFiles[i].equals(file)) {
                        // 調用newParser方法去構建一個XmlResourceParser對象,返回給調用者
                        return cachedXmlBlocks[i].newParser(id);
                    }
                }

                // 若是緩存中沒有,則建立XmlBlock,並將它放到緩存中
                // XmlBlock是已編譯的xml文件的一個包裝類
                final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
                if (block != null) {
                    final int pos = (mLastCachedXmlBlockIndex + 1) % num;
                    mLastCachedXmlBlockIndex = pos;
                    final XmlBlock oldBlock = cachedXmlBlocks[pos];
                    if (oldBlock != null) {
                        oldBlock.close();
                    }
                    cachedXmlBlockCookies[pos] = assetCookie;
                    cachedXmlBlockFiles[pos] = file;
                    cachedXmlBlocks[pos] = block;
                    // 調用newParser方法去構建一個XmlResourceParser對象,返回給調用者
                    return block.newParser(id);
                }
            }
        } catch (Exception e) {
            final NotFoundException rnf = new NotFoundException("File " + file
                    + " from xml type " + type + " resource ID #0x" + Integer.toHexString(id));
            rnf.initCause(e);
            throw rnf;
        }
    }

    throw new NotFoundException("File " + file + " from xml type " + type + " resource ID #0x"
            + Integer.toHexString(id));
}
複製代碼

首先從緩存中查找xml資源以後調用newParser方法,若是緩存中沒有,則調用AssetManger的openXmlBlockAsset方法建立一個XmlBlock,並將它放到緩存中,XmlBlock是已編譯的xml文件的一個包裝類 frameworks/base/core/java/android/content/res/AssetManager.java

XmlBlock openXmlBlockAsset(int cookie, @NonNull String fileName) throws IOException {
    Preconditions.checkNotNull(fileName, "fileName");
    synchronized (this) {
        ensureOpenLocked();
        // 調用native方法nativeOpenXmlAsset, 加載指定的xml資源文件,獲得ResXMLTree
        // xmlBlock是ResXMLTree對象的地址
        final long xmlBlock = nativeOpenXmlAsset(mObject, cookie, fileName);
        if (xmlBlock == 0) {
            throw new FileNotFoundException("Asset XML file: " + fileName);
        }
        // 建立XmlBlock,封裝xmlBlock,返回給調用者
        final XmlBlock block = new XmlBlock(this, xmlBlock);
        incRefsLocked(block.hashCode());
        return block;
    }
}
複製代碼

最終調用native方法nativeOpenXmlAsset去打開指定的xml文件,加載對應的資源,來查看一下navtive方法NativeOpenXmlAsset frameworks/base/core/jni/android_util_AssetManager.cpp

// java方法對應的native方法
{"nativeOpenXmlAsset", "(JILjava/lang/String;)J", (void*)NativeOpenXmlAsset}
    
static jlong NativeOpenXmlAsset(JNIEnv* env, jobject /*clazz*/, jlong ptr, jint jcookie,
                                jstring asset_path) {
  ApkAssetsCookie cookie = JavaCookieToApkAssetsCookie(jcookie);
  ...
  
  const DynamicRefTable* dynamic_ref_table = assetmanager->GetDynamicRefTableForCookie(cookie);

  std::unique_ptr<ResXMLTree> xml_tree = util::make_unique<ResXMLTree>(dynamic_ref_table);
  status_t err = xml_tree->setTo(asset->getBuffer(true), asset->getLength(), true);
  asset.reset();
  ...
  
  return reinterpret_cast<jlong>(xml_tree.release());
}
複製代碼
  • C++層的NativeOpenXmlAsset方法會建立ResXMLTree對象,返回的是ResXMLTree在C++層的地址
  • Java層nativeOpenXmlAsset方法的返回值xmlBlock是C++層的ResXMLTree對象的地址,而後將xmlBlock封裝進XmlBlock中返回給調用者

當xmlBlock建立以後,會調用newParser方法,構建一個XmlResourceParser對象,返回給調用者

2.5 ResourcesImpl -> XmlBlock

XmlBlock是已編譯的xml文件的一個包裝類,XmlResourceParser 負責對xml的標籤進行遍歷解析的,它的真正的實現是XmlBlock的內部類XmlBlock.Parser,而真正完成xml的遍歷操做的函數都是由XmlBlock來實現的,爲了提高效率都是經過JNI調用native的函數來作的,接下來查看一下newParser方法 frameworks/base/core/java/android/content/res/XmlBlock.java

public XmlResourceParser newParser(@AnyRes int resId) {
    synchronized (this) {
        // mNative是C++層的ResXMLTree對象的地址
        if (mNative != 0) {
            // nativeCreateParseState方法根據 mNative 查找到ResXMLTree,
            // 在C++層構建一個ResXMLParser對象,
            // 構建Parser,封裝ResXMLParser,返回給調用者
            return new Parser(nativeCreateParseState(mNative, resId), this);
        }
        return null;
    }
}
複製代碼

這個方法作兩件事

  • mNative是C++層的ResXMLTree對象的地址,調用native方法nativeCreateParseState,在C++層構建一個ResXMLParser對象,返回ResXMLParser對象在C++層的地址
  • Java層拿到ResXMLParser在C++層地址,構建Parser,封裝ResXMLParser,返回給調用者

接下來查看一下native方法nativeCreateParseState frameworks/base/core/jni/android_util_XmlBlock.cpp

// java方法對應的native方法
{ "nativeCreateParseState",     "(JI)J",
            (void*) android_content_XmlBlock_nativeCreateParseState }
            
            
static jlong android_content_XmlBlock_nativeCreateParseState(JNIEnv* env, jobject clazz,
                                                          jlong token, jint res_id)
{
    ResXMLTree* osb = reinterpret_cast<ResXMLTree*>(token);
    if (osb == NULL) {
        jniThrowNullPointerException(env, NULL);
        return 0;
    }

    ResXMLParser* st = new ResXMLParser(*osb);
    if (st == NULL) {
        jniThrowException(env, "java/lang/OutOfMemoryError", NULL);
        return 0;
    }

    st->setSourceResourceId(res_id);
    st->restart();

    return reinterpret_cast<jlong>(st);
}
複製代碼
  • token對應Java層mNative,是C++層的ResXMLTree對象的地址
  • 調用C++層android_content_XmlBlock_nativeCreateParseState方法,根據token找到ResXMLTree對象
  • 在C++層構建一個ResXMLParser對象,返給Java層對應ResXMLParser對象在C++層的地址
  • Java層拿到ResXMLParser在C++層地址,封裝到Parser中

2.6 再次回到LayoutInflater

通過一系列的跳轉,最後調用XmlBlock.newParser方法獲取資源解析器 XmlResourceParser,以後回到LayoutInflater調用處inflate方法,而後調用rInflate方法解析View frameworks/base/core/java/android/view/LayoutInflater.java

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        // 獲取context
        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        // 存儲根佈局
        View result = root;

        try {
            // 處理 START_TA G和 END_TAG
            advanceToRootNode(parser);
            final String name = parser.getName();

            // 解析merge標籤,rInflate方法會將merge標籤下面的全部子view添加到根佈局中
            // 這也是爲何merge標籤能夠簡化佈局的效果
            if (TAG_MERGE.equals(name)) {
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }
                // 解析merge標籤下的全部的view,添加到根佈局中
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // 若是不是merge標籤,調用createViewFromTag方法解析佈局視圖,這裏的temp實際上是咱們xml裏的top view
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                ViewGroup.LayoutParams params = null;

                // 若是根佈局不爲空的話,且attachToRoot爲false,爲view設置佈局參數
                if (root != null) {
                    // 獲取根佈局的LayoutParams
                    params = root.generateLayoutParams(attrs);
                    // attachToRoot爲false,爲view設置LayoutParams
                    if (!attachToRoot) {
                        temp.setLayoutParams(params);
                    }
                }

                // 解析當前view下面的全部子view
                rInflateChildren(parser, temp, attrs, true);

                // 若是 root 不爲空且 attachToRoot 爲false,將解析出來的view 添加到根佈局
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }

                // 若是根佈局爲空 或者 attachToRoot 爲false,返回當前的view
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        } catch (XmlPullParserException e) {
            final InflateException ie = new InflateException(e.getMessage(), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } catch (Exception e) {
            throw ie;
        } finally {
        }
        return result;
    }
}
複製代碼
  • 解析merge標籤,使用merge標籤必須有父佈局,且依賴於父佈局加載
  • rInflate方法會將merge標籤下面的全部view添加到根佈局中
  • 若是不是merge標籤,調用createViewFromTag解析佈局視圖,返回temp, 這裏的temp實際上是咱們xml裏的top view
  • 調用rInflateChildren方法,傳遞參數temp,在rInflateChildren方法裏內部,會調用rInflate方法, 解析當前View下面的全部子View

經過分析源碼知道了attachToRoot 和root的參數表明什麼意思,這裏總結一下:*

  • 當attachToRoot == true且root != null時,新解析出來的View會被add到root中去,而後將root做爲結果返回
  • 當attachToRoot == false且root != null時,新解析的View會直接做爲結果返回,並且root會爲新解析的View生成LayoutParams並設置到該View中去
  • 當attachToRoot == false且root == null時,新解析的View會直接做爲結果返回

不管是否是merge標籤,最後都會調用rInflate方法進行view樹的解析,他們的區別在於,若是是merge標籤傳遞的參數finishInflate是false,若是不是merge標籤傳遞的參數finishInflate是true frameworks/base/core/java/android/view/LayoutInflater.java

void rInflate(XmlPullParser parser, View parent, Context context,
        AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

    // 獲取數的深度
    final int depth = parser.getDepth();
    int type;
    boolean pendingRequestFocus = false;
    // 逐個 view 解析
    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();

        if (TAG_REQUEST_FOCUS.equals(name)) {
            // 解析android:focusable="true", 獲取view的焦點
            pendingRequestFocus = true;
            consumeChildElements(parser);
        } else if (TAG_TAG.equals(name)) {
            // 解析android:tag標籤
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            // 解析include標籤,include標籤不能做爲根佈局
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            // merge標籤必須做爲根佈局
            throw new InflateException("<merge /> must be the root element");
        } else {
            // 根據元素名解析,生成view
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            // rInflateChildren方法內部調用的rInflate方法,深度優先遍歷解析全部的子view
            rInflateChildren(parser, view, attrs, true);
            // 添加解析的view
            viewGroup.addView(view, params);
        }
    }

    if (pendingRequestFocus) {
        parent.restoreDefaultFocus();
    }

    // 若是finishInflate爲true,則調用onFinishInflate方法
    if (finishInflate) {
        parent.onFinishInflate();
    }
}
複製代碼

整個view樹的解析過程以下:

  • 獲取view樹的深度
  • 逐個 view 解析
  • 解析android:focusable="true", 獲取view的焦點
  • 解析android:tag標籤
  • 解析include標籤,而且include標籤不能做爲根佈局
  • 解析merge標籤,而且merge標籤必須做爲根佈局
  • 根據元素名解析,生成對應的view
  • rInflateChildren方法內部調用的rInflate方法,深度優先遍歷解析全部的子view
  • 添加解析的view

注意:經過分析源碼, 如下幾點須要特別注意

  • include標籤不能做爲根元素,須要放在ViewGroup中
  • merge標籤必須爲根元素,使用merge標籤必須有父佈局,且依賴於父佈局加載
  • 當XmlResourseParser對xml的遍歷,隨着佈局越複雜,層級嵌套越多,所花費的時間也越長,因此對佈局的優化,可使用meger標籤減小層級的嵌套

在解析過程當中調用createViewFromTag方法,根據元素名解析,生成對應的view,接下來查看一下createViewFromTag方法 frameworks/base/core/java/android/view/LayoutInflater.java

private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
    return createViewFromTag(parent, name, context, attrs, false);
}

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
    if (name.equals("view")) {
        name = attrs.getAttributeValue(null, "class");
    }

    // 若是設置了theme, 構建一個ContextThemeWrapper
    if (!ignoreThemeAttr) {
        final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
        final int themeResId = ta.getResourceId(0, 0);
        if (themeResId != 0) {
            context = new ContextThemeWrapper(context, themeResId);
        }
        ta.recycle();
    }

    try {
        // 若是name是blink,則建立BlinkLayout
        // 若是設置factory,根據factory進行解析, 這是系統留給咱們的Hook入口
        View view = tryCreateView(parent, name, context, attrs);

        // 若是 tryCreateView方法返回的view爲空,則判斷是內置View仍是自定義view
        // 若是是內置的View則調用onCreateView方法,若是是自定義view 則調用createView方法
        if (view == null) {
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
                // 若是使用自定義View,須要在xml指定全路徑的,
                // 例如:com.hi.dhl.CustomView,那麼這裏就有個.了
                // 能夠利用這一點斷定是內置的View,仍是自定義View
                if (-1 == name.indexOf('.')) {
                    // 解析內置view
                    view = onCreateView(context, parent, name, attrs);
                } else {
                    // 解析自定義view
                    view = createView(context, name, null, attrs);
                }
                /**
                 * onCreateView方法與createView方法的區別
                 * onCreateView方法:會給內置的View前面加一個前綴,例如:android.widget,最終會調用createView方法
                 * createView方法: 據完整的類的路徑名利用反射機制構建View對象
                 */
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }

        return view;
    } catch (InflateException e) {
        throw e;

    } catch (ClassNotFoundException e) {
        throw ie;

    } catch (Exception e) {
        throw ie;
    }
}
複製代碼
  • 解析view標籤,若是設置了theme, 構建一個ContextThemeWrapper
  • 調用tryCreateView方法,若是name是blink,則建立BlinkLayout,若是設置factory,根據factory進行解析,這是系統留給咱們的Hook入口,咱們能夠人爲的干涉系統建立View,添加更多的功能
  • 若是tryCreateView方法返回的view爲空,則分別調用onCreateView方法和 createView方法,onCreateView方法解析內置view,createView方法解析自定義view

在解析過程當中,會先調用tryCreateView方法,來看一下tryCreateView方法內部作了什麼 frameworks/base/core/java/android/view/LayoutInflater.java

public final View tryCreateView(@Nullable View parent, @NonNull String name,
    @NonNull Context context,
    @NonNull AttributeSet attrs) {
    // BlinkLayout它是FrameLayout的子類,是LayoutInflater中的一個內部類,
    // 若是當前標籤爲TAG_1995,則建立一個隔500毫秒閃爍一次的BlinkLayout來承載它的佈局內容
    if (name.equals(TAG_1995)) {
        // Let's party like it's 1995!
        // 源碼註釋也頗有意思,寫了Let's party like it's 1995!, 聽說是爲了慶祝1995年的復活節
        return new BlinkLayout(context, attrs);
    }
        
    // 若是設置factory,根據factory進行解析, 這是系統留給咱們的Hook入口,咱們能夠人爲的干涉系統建立View,添加更多的功能
    if (mFactory2 != null) {
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }

    if (view == null && mPrivateFactory != null) {
        view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }

    return view;
}
複製代碼
  • 若是name是blink,則建立BlinkLayout,返給調用者
  • 若是設置factory,根據factory進行解析, 這是系統留給咱們的Hook入口,咱們能夠人爲的干涉系統建立View,添加更多的功能,例如夜間模式,將view返給調用者

根據剛纔的分析,會先調用tryCreateView方法,若是這個方法返回的view爲空,而後會調用onCreateView方法對內置View進行解析,createView方法對自定義View進行解析

onCreateView方法與createView方法的有什麼區別

  • onCreateView方法:會給內置的View前面加一個前綴,例如:android.widget,最終會調用createView方法
  • createView方法: 根據完整的類的路徑名利用反射機制構建View對象

來看一下這兩個方法的實現,LayoutInflater是一個抽象類,咱們實際使用的是 PhoneLayoutInflater,它的結構以下

PhoneLayoutInflater重寫了LayoutInflater的onCreatView方法,這個方法就是給內置的View前面加一個前綴 frameworks/base/core/java/com/android/internal/policy/PhoneLayoutInflater.java

private static final String[] sClassPrefixList = {
    "android.widget.",
    "android.webkit.",
    "android.app."
};
    
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
    for (String prefix : sClassPrefixList) {
        try {
            View view = createView(name, prefix, attrs);
            if (view != null) {
                return view;
            }
        } catch (ClassNotFoundException e) {
           
        }
    }

    return super.onCreateView(name, attrs);
}
複製代碼

onCreateView方法會給內置的View前面加一個前綴,以後調用createView方法,真正的View構建仍是在LayoutInflater的createView方法裏完成的,createView方法根據完整的類的路徑名利用反射機制構建View對象 frameworks/base/core/java/android/view/LayoutInflater.java

public final View createView(@NonNull Context viewContext, @NonNull String name,
        @Nullable String prefix, @Nullable AttributeSet attrs)
        throws ClassNotFoundException, InflateException {
    ...

    try {

        if (constructor == null) {
            // 若是在緩存中沒有找到構造函數,則根據完整的類的路徑名利用反射機制構建View對象
            clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                    mContext.getClassLoader()).asSubclass(View.class);

            if (mFilter != null && clazz != null) {
                boolean allowed = mFilter.onLoadClass(clazz);
                if (!allowed) {
                    failNotAllowed(name, prefix, viewContext, attrs);
                }
            }
            // 利用反射機制構建clazz, 將它的構造函數存入sConstructorMap中,下次能夠直接從緩存中查找
            constructor = clazz.getConstructor(mConstructorSignature);
            constructor.setAccessible(true);
            sConstructorMap.put(name, constructor);
        } else {
            // 若是從緩存中找到了緩存的構造函數
            if (mFilter != null) {
                Boolean allowedState = mFilterMap.get(name);
                if (allowedState == null) {
                    // 根據完整的類的路徑名利用反射機制構建View對象
                    clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                            mContext.getClassLoader()).asSubclass(View.class);

                    ...
                } else if (allowedState.equals(Boolean.FALSE)) {
                    failNotAllowed(name, prefix, viewContext, attrs);
                }
            }
        }

        ...

        try {
            // 利用構造函數,建立View
            final View view = constructor.newInstance(args);
            if (view instanceof ViewStub) {
                // 若是是ViewStub,則設置LayoutInflater
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
            }
            return view;
        } finally {
            mConstructorArgs[0] = lastContext;
        }
    } catch (NoSuchMethodException e) {
        throw ie;

    } catch (ClassCastException e) {
        
        throw ie;
    } catch (ClassNotFoundException e) {
        throw e;
    } catch (Exception e) {
       
        throw ie;
    } finally {
    }
}
複製代碼
  • 先從緩存中尋找構造函數,若是存在直接使用
  • 若是沒有找到根據完整的類的路徑名利用反射機制構建View對象

到了這裏關於Apk的佈局xml資源文件的查找和解析 -> View的生成流程到這裏就結束了

總結

那咱們就來依次來回答上面提出的幾個問題

LayoutInflater的inflate的三個參數都表明什麼意思?

  • resource:要解析的xml佈局文件Id
  • root:表示根佈局
  • attachToRoot:是否要添加到父佈局root中

resource其實很好理解就是資源Id,而root 和 attachToRoot 分別表明什麼意思:

  • 當attachToRoot == true且root != null時,新解析出來的View會被add到root中去,而後將root做爲結果返回
  • 當attachToRoot == false且root != null時,新解析的View會直接做爲結果返回,並且root會爲新解析的View生成LayoutParams並設置到該View中去
  • 當attachToRoot == false且root == null時,新解析的View會直接做爲結果返回

系統對merge、include是如何處理的

  • 使用merge標籤必須有父佈局,且依賴於父佈局加載
  • merge並非一個ViewGroup,也不是一個View,它至關於聲明瞭一些視圖,等待被添加,解析過程當中遇到merge標籤會將merge標籤下面的全部子view添加到根佈局中
  • merge標籤在 XML 中必須是根元素
  • 相反的include不能做爲根元素,須要放在一個ViewGroup中
  • 使用 include 標籤必須指定有效的 layout 屬性
  • 使用 include 標籤不寫寬高是沒有關係的,會去解析被 include 的 layout

merge標籤爲何能夠起到優化佈局的效果?

解析過程當中遇到merge標籤,會調用rInflate方法,部分代碼以下

// 根據元素名解析,生成對應的view
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// rInflateChildren方法內部調用的rInflate方法,深度優先遍歷解析全部的子view
rInflateChildren(parser, view, attrs, true);
// 添加解析的view
viewGroup.addView(view, params);
複製代碼

解析merge標籤下面的全部子view,而後添加到根佈局中

view是如何被實例化的?

view分爲系統view和自定義view, 經過調用onCreateView與createView方法進行不一樣的處理

  • onCreateView方法:會給內置的View前面加一個前綴,例如:android.widget,最終會調用createView方法
  • createView方法:根據完整的類的路徑名利用反射機制構建View對象

爲何複雜佈局會產生卡頓?

  • XmlResourseParser對xml的遍歷,隨着佈局越複雜,層級嵌套越多,所花費的時間也越長
  • 調用onCreateView與createView方法是經過反射建立View對象致使的耗時
  • 在 Android 10上,新增tryInflatePrecompiled方法是爲了減小XmlPullParser解析Xml的時間,可是用一個全局變量mUseCompiledView來控制是否啓用tryInflatePrecompiled方法,根據源碼分析,mUseCompiledView始終爲false,因此tryInflatePrecompiled方法目前在release版本中不可以使用

BlinkLayout是什麼?

BlinkLayout繼承FrameLayout,是一種會閃爍的佈局,被包裹的內容會一直閃爍,根據源碼註釋Let's party like it's 1995!,BlinkLayout是爲了慶祝1995年的復活節, 有興趣能夠看看 reddit 上的討論,來查看一下它的源碼是如何實現的

private static class BlinkLayout extends FrameLayout {
    private static final int MESSAGE_BLINK = 0x42;
    private static final int BLINK_DELAY = 500;

    private boolean mBlink;
    private boolean mBlinkState;
    private final Handler mHandler;

    public BlinkLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mHandler = new Handler(new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                if (msg.what == MESSAGE_BLINK) {
                    if (mBlink) {
                        mBlinkState = !mBlinkState;
                        // 每隔500ms循環調用
                        makeBlink();
                    }
                    // 觸發dispatchDraw
                    invalidate();
                    return true;
                }
                return false;
            }
        });
    }

    private void makeBlink() {
        // 發送延遲消息
        Message message = mHandler.obtainMessage(MESSAGE_BLINK);
        mHandler.sendMessageDelayed(message, BLINK_DELAY);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        mBlink = true;
        mBlinkState = true;
        makeBlink();
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mBlink = false;
        mBlinkState = true;
        // 移除消息,避免內存泄露
        mHandler.removeMessages(MESSAGE_BLINK);
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        if (mBlinkState) {
            super.dispatchDraw(canvas);
        }
    }
}
複製代碼

經過源碼分析能夠看出,BlinkLayout 經過 Handler 每隔500ms發送消息,在 handleMessage 中循環調用 invalidate 方法,經過調用 invalidate 方法,來觸發 dispatchDraw 方法,作到一閃一閃的效果

參考

結語

致力於分享一系列的Android系統源碼、逆向分析、算法相關的文章,每篇文章都會反覆檢查以後纔會發佈,若是你同我同樣喜歡研究Android源碼,一塊兒來學習,期待與你一塊兒成長

系列文章

Android 10 源碼系列:

工具系列:

逆向系列:

相關文章
相關標籤/搜索