Android性能优化 | 把构建布局耗时缩短 20 倍(上)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: xml 布局文件是如何变成 View 并填入 View 树的?带着这个问题,阅读源码,居然发现了一个优化布局构建时间的方案。

xml 布局文件是如何变成 View 并填入 View 树的?带着这个问题,阅读源码,居然发现了一个优化布局构建时间的方案。

这是 Android 性能优化系列文章的第三篇,文章列表如下:

  1. Android性能优化 | 帧动画OOM?优化帧动画之 SurfaceView逐帧解析
  2. Android性能优化 | 大图做帧动画卡顿?优化帧动画之 SurfaceView滑动窗口式帧复用
  3. Android性能优化 | 把构建布局用时缩短 20 倍(上)
  4. Android性能优化 | 把构建布局用时缩短 20 倍(下)

布局构建耗时是优化 Activity 启动速度中不可缺少的一个环节。

欲优化,先度量。有啥办法可以精确地度量布局耗时?

读布局文件

以熟悉的setContentView()为切入点,看看有没有突破口:

public class AppCompatActivity
    @Override
    public void setContentView(View view) {
        getDelegate().setContentView(view);
    }
}

点开setContentView()源码,它的实现交给了一个代理,沿着调用链往下追查,最终的实现代码在AppCompatDelegateImpl中:

class AppCompatDelegateImpl{
    @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        //'1.从顶层视图获得content视图'
        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
        //'2.移除所有子视图'
        contentParent.removeAllViews();
        //'3.解析布局文件并填充到content视图中'
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mAppCompatWindowCallback.getWrapped().onContentChanged();
    }
}

这三部中,最耗时操作应该是“解析布局文件”,点进去看看:

public abstract class LayoutInflater {
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        ...
        //'获取布局文件解析器'
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            //'填充布局'
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
}

先调用了getLayout()获取了和布局文件对应的解析器,沿着调用链继续追查:

public class ResourcesImpl {
    XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,@NonNull String type) throws NotFoundException {
        if (id != 0) {
            try {
                synchronized (mCachedXmlBlocks) {
                    ...
                    //'通过AssetManager获取布局文件对象'
                    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;
                        return block.newParser();
                    }
                }
            } catch (Exception e) {
                ...
            }
        }
        ...
    }
}

沿着调用链,最终走到了ResourcesImpl.loadXmlResourceParser(),它通过AssetManager.openXmlBlockAsset()将 xml 布局文件转化成 Java 对象XmlBlock

public final class AssetManager implements AutoCloseable {
    @NonNull XmlBlock openXmlBlockAsset(int cookie, @NonNull String fileName) throws IOException {
        Preconditions.checkNotNull(fileName, ”fileName“);
        synchronized (this) {
            ensureOpenLocked();
            //'打开 xml 布局文件'
            final long xmlBlock = nativeOpenXmlAsset(mObject, cookie, fileName);
            if (xmlBlock == 0) {
                //'若打开失败则抛文件未找到异常'
                throw new FileNotFoundException(“Asset XML file: ” + fileName);
            }
            final XmlBlock block = new XmlBlock(this, xmlBlock);
            incRefsLocked(block.hashCode());
            return block;
        }
    }
}

通过一个 native 方法,将布局文件读取到内存。走查到这里,有一件事可以确定,即 “解析 xml 布局文件前需要进行 IO 操作,将其读取至内存中”

解析布局文件

读原码就好像“递归”,刚才通过不断地“递”,现在通过“归”回到那个关键方法:

public abstract class LayoutInflater {
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        ...
        //'获取布局文件解析器'
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            //'填充布局'
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
}

通过 IO 操作将布局文件读到内存后,调用了inflate()

public abstract class LayoutInflater {
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            ...
            try {
                    //'根据布局文件的声明控件的标签构建 View'
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    //'构建 View 对应的布局参数'
                    if (root != null) {
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

                    ...
                    //'将 View 填充到 View 树'
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
                    ...
            } catch (XmlPullParserException e) {
                ...
            }  finally {
                ...
            }
            return result;
        }
    }

这个方法解析布局文件并根据其中声明控件的标签构建 View实例,然后将其填充到 View 树中。解析布局文件的细节在createViewFromTag()中:

public abstract class LayoutInflater {
    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {
        ...

        try {
            View view;
            //'通过Factory2.onCreateView()构建 View'
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            }
            ...
            return view;
        } catch (InflateException e) {
            throw e;

        } 
        ...
    }
}

onCreateView()的具体实现在AppCompatDelegateImpl中:

class AppCompatDelegateImpl{
    @Override
    public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        return createView(parent, name, context, attrs);
    }
    
    @Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        if (mAppCompatViewInflater == null) {
            TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
            String viewInflaterClassName =
                    a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
            if ((viewInflaterClassName == null){
                ...
            } else {
                try {
                    //'通过反射获取AppCompatViewInflater实例'
                    Class<?> viewInflaterClass = Class.forName(viewInflaterClassName);
                    mAppCompatViewInflater =
                            (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
                                    .newInstance();
                } catch (Throwable t) {
                    ...
                }
            }
        }

        boolean inheritContext = false;
        if (IS_PRE_LOLLIPOP) {
            inheritContext = (attrs instanceof XmlPullParser)
                    // If we have a XmlPullParser, we can detect where we are in the layout
                    ? ((XmlPullParser) attrs).getDepth() > 1
                    // Otherwise we have to use the old heuristic
                    : shouldInheritContext((ViewParent) parent);
        }

        //'通过createView()创建View实例'
        return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
                true, /* Read read app:theme as a fallback at all times for legacy reasons */
                VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
        );
    }
}

AppCompatDelegateImpl又把构建 View 委托给了 AppCompatViewInflater.createView()

 final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        final Context originalContext = context;
        ...
        View view = null;

        //'以布局文件中控件的名称分别创建对应控件实例'
        switch (name) {
            case "TextView":
                view = createTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageView":
                view = createImageView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "Button":
                view = createButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "EditText":
                view = createEditText(context, attrs);
                verifyNotNull(view, name);
                break;
            case "Spinner":
                view = createSpinner(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageButton":
                view = createImageButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "CheckBox":
                view = createCheckBox(context, attrs);
                verifyNotNull(view, name);
                break;
            case "RadioButton":
                view = createRadioButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "CheckedTextView":
                view = createCheckedTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "AutoCompleteTextView":
                view = createAutoCompleteTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "MultiAutoCompleteTextView":
                view = createMultiAutoCompleteTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "RatingBar":
                view = createRatingBar(context, attrs);
                verifyNotNull(view, name);
                break;
            case "SeekBar":
                view = createSeekBar(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ToggleButton":
                view = createToggleButton(context, attrs);
                verifyNotNull(view, name);
                break;
            default:
                view = createView(context, name, attrs);
        }
        ...
        return view;
    }
    
    //'构建 AppCompatTextView 实例'
    protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
        return new AppCompatTextView(context, attrs);
    }
    ...
}

没想到,最终居然是通过switch-case的方法来 new View 实例。

而且我们没有必要手动将布局文件中的TextView都换成AppCompatTextView,只要使用AppCompatActivity,它在Factory2.onCreateView()接口中完成了控件转换。

测量构建布局耗时

通过上面的分析,可以得出两条结论:

1. Activity 构建布局时,需要先进行 IO 操作,将布局文件读取至内存中。

2. 遍历内存布局文件中每一个标签,并根据标签名 new 出对应视图实例,再把它们 addView 到 View 树中。

这两个步骤都是耗时的!到底有多耗时呢?

LayoutInflaterCompat提供了setFactory2(),可以拦截布局文件中每一个 View 的创建过程:

class Factory2Activity : AppCompatActivity() {
    private var sum: Double = 0.0

    @ExperimentalTime
    override fun onCreate(savedInstanceState: Bundle?) {
        LayoutInflaterCompat.setFactory2(LayoutInflater.from(this@Factory2Activity), object : LayoutInflater.Factory2 {
            
            override fun onCreateView(parent: View?, name: String?, context: Context?, attrs: AttributeSet?): View? {
                //'测量构建单个View耗时'
                val (view, duration) = measureTimedValue { delegate.createView(parent, name, context!!, attrs!!) }
                //'累加构建视图耗时'
                sum += duration.inMilliseconds
                Log.v(“test”, “view=${view?.let { it::class.simpleName }} duration=${duration}  sum=${sum}”)
                return view
            }

            //'该方法用于兼容Factory,直接返回null就好'
            override fun onCreateView(name: String?, context: Context?, attrs: AttributeSet?): View? {
                return null
            }
        })
        super.onCreate(savedInstanceState)
        setContentView(R.layout.factory2_activity2)
    }
}

super.onCreate(savedInstanceState)之前,将自定义的Factory2接口注入到LayoutInflaterCompat中。

调用delegate.createView(parent, name, context!!, attrs!!),就是手动触发源码中构建布局的逻辑。

measureTimedValue()是 Kotlin 提供的库方法,用于测量一个方法的耗时,定义如下:

public inline fun <T> measureTimedValue(block: () -> T): TimedValue<T> {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    //'委托给MonoClock'
    return MonoClock.measureTimedValue(block)
}

public inline fun <T> Clock.measureTimedValue(block: () -> T): TimedValue<T> {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }

    val mark = markNow()
    //'执行原方法'
    val result = block()
    return TimedValue(result, mark.elapsedNow())
}

public data class TimedValue<T>(val value: T, val duration: Duration)

方法返回一个TimedValue对象,其第一个属性是原方法的返回值,第二个是执行原方法的耗时。测试代码中通过解构声明分别将返回值和耗时赋值给viewduration。然后把构建每个视图的耗时累加打印。

了解了构建布局的过程,就有了对症下药优化的方向。

有了测量构建布局耗时的方法,就有了对比优化效果的工具。

限于篇幅,构建布局耗时缩短 20 倍的方法只能放到下一篇了。

目录
相关文章
|
3天前
|
存储 Java Android开发
探索安卓应用开发:构建你的第一个"Hello World"应用
【9月更文挑战第24天】在本文中,我们将踏上一段激动人心的旅程,深入安卓应用开发的奥秘。通过一个简单而经典的“Hello World”项目,我们将解锁安卓应用开发的基础概念和步骤。无论你是编程新手还是希望扩展技能的老手,这篇文章都将为你提供一次实操体验。从搭建开发环境到运行你的应用,每一步都清晰易懂,确保你能顺利地迈出安卓开发的第一步。让我们开始吧,探索如何将一行简单的代码转变为一个功能齐全的安卓应用!
|
30天前
|
移动开发 监控 前端开发
构建高效Android应用:从优化布局到提升性能
【7月更文挑战第60天】在移动开发领域,一个流畅且响应迅速的应用程序是用户留存的关键。针对Android平台,开发者面临的挑战包括多样化的设备兼容性和性能优化。本文将深入探讨如何通过改进布局设计、内存管理和多线程处理来构建高效的Android应用。我们将剖析布局优化的细节,并讨论最新的Android性能提升策略,以帮助开发者创建更快速、更流畅的用户体验。
50 10
|
16天前
|
存储 Java 编译器
🔍深入Android底层,揭秘JVM与ART的奥秘,性能优化新视角!🔬
【9月更文挑战第12天】在Android开发领域,深入了解其底层机制对提升应用性能至关重要。本文详述了从早期Dalvik虚拟机到现今Android Runtime(ART)的演变过程,揭示了ART通过预编译技术实现更快启动速度和更高执行效率的奥秘。文中还介绍了ART的编译器与运行时环境,并提出了减少DEX文件数量、优化代码结构及合理管理内存等多种性能优化策略。通过掌握这些知识,开发者可以从全新的角度提升应用性能。
39 11
|
17天前
|
开发框架 Android开发 iOS开发
探索安卓与iOS开发的差异:构建未来应用的指南
在移动应用开发的广阔天地中,安卓与iOS两大平台各占半壁江山。本文将深入浅出地对比这两大操作系统的开发环境、工具和用户体验设计,揭示它们在编程语言、开发工具以及市场定位上的根本差异。我们将从开发者的视角出发,逐步剖析如何根据项目需求和目标受众选择适合的平台,同时探讨跨平台开发框架的利与弊,为那些立志于打造下一个热门应用的开发者提供一份实用的指南。
40 5
|
30天前
|
人工智能 缓存 数据库
安卓应用开发中的性能优化技巧AI在医疗诊断中的应用
【8月更文挑战第29天】在安卓开发的广阔天地里,性能优化是提升用户体验、确保应用流畅运行的关键所在。本文将深入浅出地探讨如何通过代码优化、资源管理和异步处理等技术手段,有效提升安卓应用的性能表现。无论你是初学者还是资深开发者,这些实用的技巧都将为你的安卓开发之路增添光彩。
|
30天前
|
编解码 Android开发
【Android Studio】使用UI工具绘制,ConstraintLayout 限制性布局,快速上手
本文介绍了Android Studio中使用ConstraintLayout布局的方法,通过创建布局文件、设置控件约束等步骤,快速上手UI设计,并提供了一个TV Launcher界面布局的绘制示例。
34 1
|
1月前
|
API Android开发
Android P 性能优化:创建APP进程白名单,杀死白名单之外的进程
本文介绍了在Android P系统中通过创建应用进程白名单并杀死白名单之外的进程来优化性能的方法,包括设置权限、获取运行中的APP列表、配置白名单以及在应用启动时杀死非白名单进程的代码实现。
45 1
|
19天前
|
开发工具 Android开发 iOS开发
探索安卓与iOS开发的差异:构建未来应用的关键考量
在数字时代的浪潮中,安卓和iOS这两大操作系统如同双子星座般耀眼夺目,引领着移动应用的潮流。它们各自拥有独特的魅力和深厚的用户基础,为开发者提供了广阔的舞台。然而,正如每枚硬币都有两面,安卓与iOS在开发过程中也展现出了截然不同的特性。本文将深入剖析这两者在开发环境、编程语言、用户体验设计等方面的显著差异,并探讨如何根据目标受众和项目需求做出明智的选择。无论你是初涉移动应用开发的新手,还是寻求拓展技能边界的资深开发者,这篇文章都将为你提供宝贵的见解和实用的建议,帮助你在安卓与iOS的开发之路上更加从容自信地前行。
|
27天前
|
图形学 iOS开发 Android开发
从Unity开发到移动平台制胜攻略:全面解析iOS与Android应用发布流程,助你轻松掌握跨平台发布技巧,打造爆款手游不是梦——性能优化、广告集成与内购设置全包含
【8月更文挑战第31天】本书详细介绍了如何在Unity中设置项目以适应移动设备,涵盖性能优化、集成广告及内购功能等关键步骤。通过具体示例和代码片段,指导读者完成iOS和Android应用的打包与发布,确保应用顺利上线并获得成功。无论是性能调整还是平台特定的操作,本书均提供了全面的解决方案。
104 0
|
27天前
|
Android开发 iOS开发 C#
Xamarin:用C#打造跨平台移动应用的终极利器——从零开始构建你的第一个iOS与Android通用App,体验前所未有的高效与便捷开发之旅
【8月更文挑战第31天】Xamarin 是一个强大的框架,允许开发者使用单一的 C# 代码库构建高性能的原生移动应用,支持 iOS、Android 和 Windows 平台。作为微软的一部分,Xamarin 充分利用了 .NET 框架的强大功能,提供了丰富的 API 和工具集,简化了跨平台移动应用开发。本文通过一个简单的示例应用介绍了如何使用 Xamarin.Forms 快速创建跨平台应用,包括设置开发环境、定义用户界面和实现按钮点击事件处理逻辑。这个示例展示了 Xamarin.Forms 的基本功能,帮助开发者提高开发效率并实现一致的用户体验。
69 0