「Android10源码分析」为什么复杂布局会产生卡顿?-- LayoutInflater详解

28,077 阅读10分钟

系列文章索引

Android系统启动流程

  1. 源码下载及编译
  2. Android系统启动流程纵览
  3. init进程源码解析
  4. zygote进程源码解析
  5. systemServer源码解析

LayoutInflater源码详解

更新

录播回放已上传,请戳链接食用:【Android/源码/面试】LayoutInflater源码详解

前言

这篇文章会从源码的角度分析,LayoutInflater将xml文件实例化为一个view对象的流程

我们会发现,其中有两个部分是耗时的主要来源

  1. XmlResourseParser对xml的遍历
  2. 反射创建View对象导致的耗时

这两点,又跟Xml的复杂程度成正相关,Xml越复杂,则递归调用所消耗的时间就越长,就产生了我们所说的,卡顿问题

整体流程概览

彩蛋:BlinkLayout

BlinkLayout是LayoutInflater中的一个内部类,它本身是是FrameLayout的子类,如果当前标签为TAG_1995,则创建一个隔0.5秒闪烁一次的BlinkLayout来承载它的布局内容

源码注释也很有意思,写了Let's party like it's 1995!, 据说是为了庆祝1995年的复活节

LayoutInflater

public final View tryCreateView(@Nullable View parent, @NonNull String name,
        @NonNull Context context,
        @NonNull AttributeSet attrs) {
        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995! 
            return new BlinkLayout(context, attrs);
        }
				...
        return view;
    }

具体使用也很简单

   <blink
        android:layout_below="@id/iv_qr_code"
        android:layout_centerHorizontal="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Android研习社"
            android:textColor="#157686"
            android:textSize="55sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </blink>

效果如下,这种效果也适合来做EditText中光标的闪烁效果

扫描上方二维码关注「Android研习社」公众号,获取更多学习资料!

ps: 想深入学习的都关注了,还不赶快关注一波?

LayoutInflater的创建

概览

LayoutInflater是一个抽象类,它的创建,并不是交由App层处理的,而是调用了from()的静态函数,经由系统服务LAYOUT_INFLATER_SERVICE,最终创建了一个LayoutInflater的子类对象--PhoneLayoutInflater

重要函数解析

LayoutInflater.from(cxt)

这个函数比较简单,就是根据传递过来的Context对象,调用getSystemService()来获取对应的系统服务,并赋值给LayoutInflater

public static LayoutInflater from(Context context) { 
        LayoutInflater LayoutInflater =  //LayoutInflate是一个系统服务,最终返回的是`PhoneLahyoutInflater`
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }

Context本身是一个抽象类,它真正的实例化对象,是ContextImpl, 在这个类的getSystemService()函数中,真正执行获取系统服务的类,是SystemServiceRegistry,其中又封装了一个ServiceFetcher来获取真正的系统服务,所有的系统服务,都是存储在一个map集合--SYSTEM_SERVICE_FETCHERS当中,这里其实是一个get方法,是从这个map集合中取出对应的系统服务

LayoutInflater

@Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }

SystemServiceRegistry

/**
     * Gets a system service from a given context.
     */
    public static Object getSystemService(ContextImpl ctx, String name) {
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }

关于对应的服务的添加,也就是调用了SYSTEM_SERVICE_FETCHERS的put函数,这个动作是交由registerService()来完成的

/**
     * Statically registers a system service with the context.
     * This method must be called during static initialization only.
     */
    private static <T> void registerService(String serviceName, Class<T> serviceClass,
            ServiceFetcher<T> serviceFetcher) {
        SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
        SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
    }

SystemServiceRegistry这个类中有一个静态代码块,是用来完成所有服务的注册的,这里我们只关心LAYOUT_INFLATER_SERVICE对应的服务是如何注册的

static{
				...
				registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                new CachedServiceFetcher<LayoutInflater>() {
            @Override
            public LayoutInflater createService(ContextImpl ctx) {
                return new PhoneLayoutInflater(ctx.getOuterContext());
            }});
        ...
}

正如我们之前所说,这里最终是创建了一个PhoneLayoutInflater并返回的,到这里LayoutInflater的创建流程就分析完了

思考

为什么要交由系统服务来做,而不是直接创建一个PhoneLayoutInflater的实例对象?

LayoutInflater布局的实例化

整体流程

实例化的调用流程我们都很熟悉了,调用layoutInflaterinflater()函数,传入一个xml的resId参数就可以了

重要函数解析

inflate

这个函数就是我们把Xml布局文件实例化为一个View对象的入口了

LayoutInflater

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                  + Integer.toHexString(resource) + ")");
        }

        View view = tryInflatePrecompiled(resource, res, root, attachToRoot); //这段代码其实是必然返回null的,因为当前版本写死了预编译的Enable为false
        if (view != null) {
            return view;
        }
        XmlResourceParser parser = res.getLayout(resource); //获取XmlBlock.Parser对象
        try {
            return inflate(parser, root, attachToRoot); 
        } finally {
            parser.close(); 
        }
    }

此处又调用了inflate(parser, root, attachToRoot)这个函数,来对Xml布局进行解析

这里看到对一些熟悉的标签,比如include,merge,的处理,具体细节请看下面的源码及注释

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {//XmlPullParser是一个接口
		//此函数是真正执行将xml解析为视图view的过程,此处的parser为根据xml布局获取到的parser对象
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final inflateAttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root; //需要返回的view对象

            try {
                advanceToRootNode(parser); //对START_TAG和END_TAG进行判断和处理
                final String name = parser.getName();  //获取当前标签

                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }

                if (TAG_MERGE.equals(name)) { //如果使用merge标签
                    if (root == null || !attachToRoot) { //使用merge标签必须有父布局,且依赖于父布局加载,否则就会抛出异常
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);//递归(Recursive)生成布局视图
                } else { //如果不使用merge标签,创建tmp 作为临时的根节点,并最终赋值给result返回
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs); ////根据标签名创建一个view

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {  //如果rootView不为空
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);  //根据rootView生成layoutparams
                        if (!attachToRoot) { //如果attachToRoot为false
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);  //设置一个临时的params
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }

                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) { //如果root不为空,且attachToRoot为true
                        root.addView(temp, params); //把temp添加到到rootview中
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) { // 如果root为空且attachToRoot为false
                        result = temp; //将temp,也就是根结点的View赋值给result
                    }
                }

            } 
						...
            return result;  //返回结果
		}
  }

rInflate

从上面的代码中我们也可以看到,不管是merge标签,还是非merge标签,最终都会调用到rInflate()这个函数,这个是用于递归向下遍历xml布局,最终调用createViewFromTag()函数来反射创建View对象

具体细节请看下面的源码及注释

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;

        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)) { //如果需REQUEST_FOCUS标签
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } else if (TAG_TAG.equals(name)) { //如果是“tag”标签
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) { //如果是 Include标签
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, context, parent, attrs); //对 include标签进行解析
            } else if (TAG_MERGE.equals(name)) { //如果是merge标签
                throw new InflateException("<merge /> must be the root element"); //直接抛出异常
            } else { //其他标签
                final View view = createViewFromTag(parent, name, context, attrs); //根据Tag创建view,反射创建View
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflateChildren(parser, view, attrs, true); //递归调用rInflate函数
                viewGroup.addView(view, params);
            }
        }

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

        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

createViewFromTag()

终于到了我们的重头戏,也是真正根据解析到的Tag标签去反射创建View的函数

LayoutInflater

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

        // Apply a theme wrapper, if allowed and one is specified.
        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 {
            View view = tryCreateView(parent, name, context, attrs); //尝试使用Factory来闯将View对象

            if (view == null) { //如果tryCreateView返回null
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
					//sample:com.aiwinn.base.widget.CameraSurfaceView
                    if (-1 == name.indexOf('.')) {  //如果当前Tag含有“.”
                        view = onCreateView(context, parent, name, attrs);
                    } else {
                        view = createView(context, name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } 
  			...
    }

在这个函数中会首先调用tryCreateView()来获取View对象,如果为null,则进一步调用createView()函数来创建View对象

public final View createView(@NonNull Context viewContext, @NonNull String name,
            @Nullable String prefix, @Nullable AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
				//根据Tag反射创建view
        Objects.requireNonNull(viewContext);
        Objects.requireNonNull(name);
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;

        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                        mContext.getClassLoader()).asSubclass(View.class);//把prefix和name进行拼接,获取到对应的Class对象

                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, viewContext, attrs);
                    }
                }
                constructor = clazz.getConstructor(mConstructorSignature);//获取构构造器对象
                constructor.setAccessible(true);
                sConstructorMap.put(name, constructor);
            } else {
                ...
            }

            Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = viewContext;
            Object[] args = mConstructorArgs;
            args[1] = attrs;

            try {
                final View view = constructor.newInstance(args); //根据获取到的构造器创建一个View的实例化对象
                if (view instanceof ViewStub) {
                    // Use the same context when inflating ViewStub later.
                    final ViewStub viewStub = (ViewStub) view;
                    viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
                }
                return view;
            } finally {
                mConstructorArgs[0] = lastContext;
            }catch{
              	...
            }
        } 
    }

这里的代码其实在耗时上算是比较重量级了,因为是使用反射来创建的,一般的说法是,反射比直接创建对象要慢3倍,iReaderx2c框架就是基于这一点去做的优化

加餐

Resources的创建和获取

这里先获取到Resources对象--mResources,这个对象的创建是由createResources()来完成的,这里最终是交由ResourcesManager这个类来获取对应的resources

ContextImpl

private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName,
            int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) {
        final String[] splitResDirs;
        final ClassLoader classLoader;
        try {
            splitResDirs = pi.getSplitPaths(splitName);
            classLoader = pi.getSplitClassLoader(splitName);
        } catch (NameNotFoundException e) {
            throw new RuntimeException(e);
        }
        return ResourcesManager.getInstance().getResources(activityToken,
                pi.getResDir(),
                splitResDirs,
                pi.getOverlayDirs(),
                pi.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfig,
                compatInfo,
                classLoader);
    }

ResourcesManager

public @Nullable Resources getResources(@Nullable IBinder activityToken,
            @Nullable String resDir,
            @Nullable String[] splitResDirs,
            @Nullable String[] overlayDirs,
            @Nullable String[] libDirs,
            int displayId,
            @Nullable Configuration overrideConfig,
            @NonNull CompatibilityInfo compatInfo,
            @Nullable ClassLoader classLoader) {
        try {
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
            final ResourcesKey key = new ResourcesKey(
                    resDir,
                    splitResDirs,
                    overlayDirs,
                    libDirs,
                    displayId,
                    overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
                    compatInfo);
            classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
            return getOrCreateResources(activityToken, key, classLoader);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
    }

ContextImpl

@Override
    public Resources getResources() {
        return mResources;
    }

由这里我们也可以推断,LayoutInflater交由服务来创建来创建,是因为其需要获取系统服务才能获取的某些资源

XmlBlock

inflate()函数里还涉及到一个重要的类, XmlResourceParser,这个类是负责对xml的标签进行遍历解析的,它的真正的实现类是XmlBlock的内部类XmlBlock.Parser,而真正完成xml的遍历操作的函数都是由XmlBlock来实现的,为了提升效率,该函数都是通过JNI调用native的函数来做的,对应的native层是android_util_XmlBlock.cpp

XmlBlock.java

@FastNative
    /*package*/ static final native int nativeNext(long state);
    @FastNative
    private static final native int nativeGetNamespace(long state);
    @FastNative
    /*package*/ static final native int nativeGetName(long state);
    @FastNative
    private static final native int nativeGetText(long state);
    @FastNative
    private static final native int nativeGetLineNumber(long state);
   	...

``android_util_XmlBlock.cpp`

static jint android_content_XmlBlock_nativeNext(JNIEnv* env, jobject clazz,
                                             jlong token)
{
    ResXMLParser* st = reinterpret_cast<ResXMLParser*>(token);
    if (st == NULL) {
        return ResXMLParser::END_DOCUMENT;
    }

    do {
        ResXMLParser::event_code_t code = st->next();
        switch (code) {
            case ResXMLParser::START_TAG:
                return 2;
            case ResXMLParser::END_TAG:
                return 3;
            case ResXMLParser::TEXT:
                return 4;
            case ResXMLParser::START_DOCUMENT:
                return 0;
            case ResXMLParser::END_DOCUMENT:
                return 1;
            case ResXMLParser::BAD_DOCUMENT:
                goto bad;
            default:
                break;
        }
    } while (true);

bad:
    jniThrowException(env, "org/xmlpull/v1/XmlPullParserException",
            "Corrupt XML binary file");
    return ResXMLParser::BAD_DOCUMENT;
}

tryInflatePrecompiled

这个函数是Android10的源码里面新增的一个函数,是用来根据xml预编译生成的dex,通过反射来生成对应的View,从而减少XmlPullParser解析Xml的时间 -- 放到编译期来进行-- 的一个优化 ,而反射获取对应的View时可以直接获取到预编译的View对象,而不需要递归调用rInflate

这里基本上就是真正彻底解决了复杂布局导致的卡顿问题

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

        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate (precompiled)");

        // Try to inflate using a precompiled layout.
        String pkg = res.getResourcePackageName(resource);
        String layout = res.getResourceEntryName(resource);
		
		//依然是通过反射的方式,根据已经创建的mPrecompiledClassLoader来反射生成view对象
        try {
            Class clazz = Class.forName("" + pkg + ".CompiledView", false, mPrecompiledClassLoader); //获取到预编译生成的view对象的Class类
            Method inflater = clazz.getMethod(layout, Context.class, int.class);
            View view = (View) inflater.invoke(null, mContext, resource);

            if (view != null && root != null) {
                // We were able to use the precompiled inflater, but now we need to do some work to
                // attach the view to the root correctly.
                XmlResourceParser parser = res.getLayout(resource);
                try {
                    AttributeSet attrs = Xml.asAttributeSet(parser);
                    advanceToRootNode(parser);
                    ViewGroup.LayoutParams params = root.generateLayoutParams(attrs);

                    if (attachToRoot) {
                        root.addView(view, params);
                    } else {
                        view.setLayoutParams(params);
                    }
                } finally {
                    parser.close();
                }
            }
            return view;
        } catch (Throwable e) {
            if (DEBUG) {
                Log.e(TAG, "Failed to use precompiled view", e);
            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        return null;
    }

写在最后

下一篇文章,我们会提出一些优化方案,来解决(或者说)减缓复杂布局产生的卡顿问题,敬请期待!

参考文章:

1. https://www.reddit.com/r/androiddev/comments/3sekn8/lets_party_like_its_1995_from_the_layoutinflater/
2. https://www.cnblogs.com/liyilin-jack/p/10282385.html
3. https://blog.csdn.net/axi295309066/article/details/60128009
4. https://github.com/RTFSC-Android/RTFSC/blob/master/BlinkLayout.md
5. https://juejin.cn/post/6844903785219751944

郑重声明

本文版权归Android研习社所有,未经允许禁止转载,侵权必究!