为什么需要推迟视图初始化
这里谈谈为什么要推迟视图的初始化. 假设这样一个情况, 如果手机的界面包含大量的视图组件,而且数据大多都来源于服务器. 这就意味着一进入这个界面,应用就会在onCreate()方法中去初始化为数不少的layout资源, 而初始化View是一个比较耗时耗资源的操作. 然后, 应用进行几个网络链接,去获取数据回来更新填充View中的各个组件. 这样算起来, 用户从进入某个Activity, 到真的看到该Activity的视图, 所需等待的时间确实不短. 而且要知道, 应用中如果某个操作的请求超过5秒, 广播的处理时间超过10秒,
系统就会认为该请求是无法处理的, 弹出可恶的ANR弹窗,提示用户是继续等待or强制关闭这个应用. 这样用户的体验就很差了. 用户一旦不爽,就很可能卸载了这个应用. 真乃" 一念天堂,一年地狱."
当然我们可以通过不断优化应用中布局文件的设计,比如精简layout中的层级关系,减少不必要的组件, 尽量用RelativeLayout去代替嵌套的LinearLayout等等,达到我们减轻初始化View, 提高应用响应速度的目的.这些我们都会, 然而,这次我们了解另一种方案, 使用ViewStub推迟视图初始化.
我们日常应用环境, 其实随处可见这种设计思想. 打开一个网站的页面,我们会看到一个Loading的提示框. 打开手机中某款应用, 显示一个可爱的加载动画, 或者提示页面等等. 然后应用在"背后" 努力的初始化资源, 甚至还打开几个子线程去服务器读数据回来客户端更新数据等等. 用户可不关心应用实际上在做什么, 但也不会愿意一直看着白屏在那里等待.就像你在女票的楼下等女票一样, 等别人即使是多一秒, 心里也总是焦急难熬. 这就有必要进行推迟视图的初始化了.
了解ViewStub
OK, 扯多了. 我们直接来看ViewStub是何物. 通常如果一个类不是一万几千行的代码, 我是非常推荐去把类源码读完的, 这样能深入的了解一个类的作用.如果源码太多,时间不允许, 就选择需要的几个方法去读, 或者快速浏览一下类的结构方法.
ViewStub作为View的子类,是一种非常轻量化,几乎不占用内存的组件.也因为这个特点,它非常适合用来放在推迟视图的初始化中使用.在Activity的onCreate()中,系统会自动初始化ViewStub组件, 这个时候用户只会看到ViewStub. 然后应用进行数据的读取或处理, 觉得准备就绪, 就利用ViewStub.inflate()方法去初始化, 这个方法会返回真正的View对象, 也就是应用所真正呈现给用户的界面. 一旦初始化,ViewStub就会被标记为可回收的资源, 从而会被系统回收掉.
下面是一个完整的用法:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <ViewStub android:id="@+id/stub_main" android:inflatedId="@+id/stubid" android:layout="@layout/act_main" android:layout_width="400dp" android:layout_height="200dp" /> </RelativeLayout>
act_main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <ImageView android:id="@+id/image" android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/pic_default" /> </RelativeLayout>
MainActivity:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); viewStub = (ViewStub)findViewById(R.id.stub_main); <span style="white-space:pre"> </span>//获取ViewStub mView = viewStub.inflate();<span style="white-space:pre"> </span>//调用ViewStub.inflate()获取真正的View ImageView imageView = mView.findViewById(R.i.image); }
只是展示用法,因为用了很简单的例子, 应该不难理解吧. 首先是MainActivity初始化一个非常简单的界面activity_main.xml, 这个界面只有一个ViewStub,然后ViewStub里面通过inflateId作为唯一标记引用了act_main.xml. 在代码里通过viewStub.inflate() 获取这个act_main.xml的初始化View对象. 所以整个过程, 用户一开始看到的是ViewStub, 然后才是ImageView. 这个就是推迟视图初始化的过程.
值得注意的是, 所被初始化的act_main.xml.的大小,和所处同一个parent的ViewStub的具有相同大小.也可以这么理解,ViewStub的大小,决定了viewStub.inflate()所返回的View对象的长宽大小.如上面示例所示, ViewStub是宽为400dip,长200dip,因此act_main.xml的大小也是宽为400dip,长200dip.
借鉴了官方的这种思路,我们不免会觉得ViewStub所展示出来的界面太单调了.于是我们可以用自己的View去代替ViewStub. 只要我们保证一个加载页面的足够简洁(比如只是一个加载动画,或者一个温馨的提示界面等等),我们甚至不必去使用这个View的findViewById()操作.仅仅是加载一个足够简洁,不占用什么内存的View. 进一步想, 我们还能保持这个View能被各个Activity所引用,而不需要重复创建这个加载的View. 那么应用牺牲非常少的内存,也是值得的. 这个思路, 就是现在几乎所有常见应用进入某个界面都能看到统一的加载界面的设计思路.
没有最好,只有更好. 这里仅仅提供其中一种的思路, 不能说是最优方案,仅供参考.
希望本次性能优化系列的文章, 能给你一点点帮助哦~ 感谢你的阅读.
抽时间看了ViewStub的源码, 官方的注释已经非常全面的介绍了该类. 于是将我直接翻译了该类的注释,有兴趣可以看看哦.
/** * (个人认为,这个类只读官方的注释足以了解,源代码量也不大,但不必深究inflate的过程.只需要懂得ViewStub为何物,以及如何使用即可.) * *ViewStub是不可见的,不占用内存的View视图. (当程序初始化视图时)它可以用来推迟初始化视图里面的各种layout资源. * * A ViewStub is an invisible, zero-sized View that can be used to lazily inflate * layout resources at runtime. * * 当ViewStub被设定为可见或者开始初始化视图时,layout资源开始被初始化(这个时候,真正要使用的各种layout资源才开始占用内存). * 这时,ViewStub会被相同的parent中所初始化的View代替.因此,ViewStub的存在直至设置setVisibility(VISIBLE)或者调用方法去 * 初始化view(比如通过使用ViewStub.inflate()方法)时就会结束. * * When a ViewStub is made visible, or when {@link #inflate()} is invoked, the layout resource * is inflated. The ViewStub then replaces itself in its parent with the inflated View or Views. * Therefore, the ViewStub exists in the view hierarchy until {@link #setVisibility(int)} or * {@link #inflate()} is invoked. * * 初始化的View会被添加到与ViewStub相同parent的容器中.类似的,你能通过使用ViewStub的inflateId属性来定义或者覆盖视图View的id. * (就是说,将要初始化的View的ID放在ViewStub的布局文件中,比如android:inflatedId="@+id/subTree".然后使用ViewStub的inflate()方法,你就能获取这个View.) * 下面是一个布局文件的示例. * * The inflated View is added to the ViewStub's parent with the ViewStub's layout * parameters. Similarly, you can define/override the inflate View's id by using the * ViewStub's inflatedId property. For instance: * * <pre> * <ViewStub android:id="@+id/stub" * android:inflatedId="@+id/subTree" * android:layout="@layout/mySubTree" * android:layout_width="120dip" * android:layout_height="40dip" /> * </pre> * * 上面示例中,ViewStub通过使用"stub"作为id,能被应用所获取并使用.在"mySubTree"这个view里被初始化后,ViewStub就会从容器中被移除(即被标记为系统可回收的资源) * 而被初始化的View通过使用inflateId为subTree的这个属性,又能被初始化为mySubTree的View.可见,inflateId(值为subTree)是inflate过程的唯一标记.而最后被初始化的 * mySubTree这个布局文件,所占用的大小跟ViewStub是完全一样的,都是android:layout_width="120dip", android:layout_height="40dip". * * The ViewStub thus defined can be found using the id "stub." After inflation of * the layout resource "mySubTree," the ViewStub is removed from its parent. The * View created by inflating the layout resource "mySubTree" can be found using the * id "subTree," specified by the inflatedId property. The inflated View is finally * assigned a width of 120dip and a height of 40dip. * * The preferred way to perform the inflation of the layout resource is the following: * * 下面是在上面代码示例的基础上,去获取ViewStub和通过ViewStub推迟初始化的方法: * * <pre> * ViewStub stub = (ViewStub) findViewById(R.id.stub); * View inflated = stub.inflate(); * </pre> * * 当ViewStub的inflate()方法被调用时,ViewStub会被所初始化的View所代替,并且该方法会返回一个View对象. * 这使得应用程序能获取视图的引用而又不必执行额外的findViewById()操作.(因为findViewById()操作也是占用一定系统资源的.) * * When {@link #inflate()} is invoked, the ViewStub is replaced by the inflated View * and the inflated View is returned. This lets applications get a reference to the * inflated View without executing an extra findViewById(). * * @attr ref android.R.styleable#ViewStub_inflatedId * @attr ref android.R.styleable#ViewStub_layout */ @RemoteView public final class ViewStub extends View { //... }