引言
- 這是 Android 10 源碼分析系列的第 3 篇
- 分支:android-10.0.0_r14
- 全文閱讀大概 15 分鐘
- 首發于掘金:https://juejin.im/post/5e6c8c14f265da57......
通過這篇文章你將學習到以下內容,文末會給出相應的答案
- LayoutInflater的inflate 方法的三個參數都代表什么意思?
- 系統對 merge、include 是如何處理的
- merge 標簽為什么可以起到優化布局的效果?
- XML 中的 View 是如何被實例化的?
- 為什么復雜布局會產生卡頓?在 Android 10 上做了那些優化?
- BlinkLayout 是什么?
前面兩篇文章 0xA01 Android 10 源碼分析:APK 是如何生成的 和 0xA02 Android 10 源碼分析:APK 的安裝流程 分析了 APK 大概可以分為代碼和資源兩部分,那么 APK 的加載也是分為代碼和資源兩部分,代碼的加載涉及了進程的創建、啟動、調度,本文主要來分析一下資源的加載,如果沒有看過 APK 是如何生成的 和 APK 的安裝流程 可以點擊下方連接前往:
1. Android 資源
Android 資源大概分為兩個部分:assets 和 res
assets 資源
assets 資源放在 assets 目錄下,它里面保存一些原始的文件,可以以任何方式來進行組織,這些文件最終會原封不動的被打包進 APK 文件中,通過AssetManager 來獲取 asset 資源,代碼如下
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 是如何生成的
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.java
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 層 nativeOpenXmlAsse t方法的返回值 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 對象
為什么復雜布局會產生卡頓?在 Android 10 上做了那些優化?
- 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 方法,做到一閃一閃的效果
參考
- https://www.reddit.com/r/androiddev/comments/3sekn8/lets_party_like_its_1995_from_the_layoutinflater/
- https://github.com/RTFSC-Android/RTFSC/blob/master/LayoutInflater.md
- https://www.yuque.com/beesx/beesandroid/gd7w9o
結語
致力于分享一系列 Android 系統源碼、逆向分析、算法相關的文章,如果你同我一樣喜歡研究 Android 源碼,可以關注我,一起來學習,期待與你一起成長
文章列表
Android 10 源碼系列
- 0xA01 Android 10 源碼分析:APK 是如何生成的
- 0xA02 Android 10 源碼分析:APK 的安裝流程
- 0xA03 Android 10 源碼分析:APK 加載流程之資源加載
- 0xA04 Android 10 源碼分析:APK 加載流程之資源加載(二)
- 0xA05 Android 10 源碼分析:Dialog 加載繪制流程以及在 Kotlin、DataBinding 中的使用
- 0xA06 Android 10 源碼分析:WindowManager 視圖綁定以及體系結構
工具系列
- 為數不多的人知道的 AndroidStudio 快捷鍵(一)
- 為數不多的人知道的 AndroidStudio 快捷鍵(二)
- 關于 adb 命令你所需要知道的
- 如何高效獲取視頻截圖
- 10分鐘入門 Shell 腳本編程
- 如何在項目中封裝 Kotlin + Android Databinding