Android开发

Android 10 源码分析:Apk加载流程之资源加载

本文主要是介绍Android 10 源码分析:Apk加载流程之资源加载,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

引言

  • AndroidStudio:3.6.0
  • 分支:android-10.0.0_r14
  • 文中地址如果不能访问,需要科学上网
  • 全文阅读大概15分钟

通过这篇文章你将学习到以下内容:

  • LayoutInflater的inflate方法的三个参数都代表什么意思?
  • 系统对merge、include是如何处理的
  • merge标签为什么可以起到优化布局的效果?
  • xml中的view是如何被实例化的?
  • 为什么复杂布局会产生卡顿?
  • 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层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 源码系列:

工具系列:

逆向系列:

这篇关于Android 10 源码分析:Apk加载流程之资源加载的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!