Android性能优化利器:使用ViewStub优化你的布局

2,184 阅读4分钟

1 问题引入:

在开发应用程序的时候,经常会遇到这样的情况,会在运行时根据条件来决定哪个View或某个布局的显示与隐藏。你会怎么做呢?

2 解决方案:

2.1 方案1:View.setVisibility

最常见的想法就是先把View都写到布局中,把它们的可见性设为View.Gone,然后在代码更改它的可见性。

这种做法的优点就是逻辑简单而且控制起来比较灵活,但是缺点也很明显就是耗费资源(尤其布局复杂且多个这种View而且要求性能高的APP)。

虽然把View的初始化可见设置为View.Gone,但是在inflate布局的时候View任然会被inflate,也就是说还是会创建对象,会被实例化,会被设置属性,这些在一定程度上肯定会耗费内存等资源的。

2.2 方案2:ViewStub

ViewStub,正如它的名字一样,是一个占位的View,本质上来说它就是一个宽高都为0的一个View,且默认是不可见的,只有在需要的时候,调用setVisibility方法或者inflate方法才会将其要装载的目标布局给加载出来,从而达到延迟加载的效果。这样就能优化页面渲染的速度。

2.3 View.setVisibility与ViewStub对比

下表列出了View.setVisibility与ViewStub在各个阶段的对比,可以看出性能方面: ViewStub>View.Gone>View.INVISIBLE>View.View.Visible

image.png

3 ViewStub的用法

布局中使用ViewStub占位

<ViewStub
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:layout_marginStart="16dp"
    android:layout_marginTop="16dp"
    android:layout_marginEnd="16dp"
    android:id="@+id/view_stub_tv"
    android:inflatedId="@+id/tv_inflate"
    android:layout="@layout/view_stub_content_text_layout"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

android:id="@+id/view_stub_tv": ViewStub的id android:inflatedId="@+id/tv_inflate" :被装载的View的id android:layout="@layout/view_stub_content_text_layout":被装载的View的布局

view_stub_content_text_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/tv_view_stub_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:text="这是 view stub 真正的内容">
</TextView>

注意:即使被装载的View布局中有id也是无效的,还是要用使用android:inflated中声明的id。

4 装载:

ViewStub本质是一个空View,因此可以像其他View一样通过findViewById拿到。

val tvStub= findViewById<ViewStub>(R.id.view_stub_tv)

此时它还是一个ViewStub,并不是真正装载的View。我们使用ViewStub的目的就是在需要的时机才去加载View,以达到延迟加载的目的。

ViewStub加载真正的View有两种方式:

4.1 ViewStub.inflate方法:

tvView = findViewById<ViewStub>(R.id.view_stub_tv).inflate() as TextView

注意,inflate后产生的对象类型是View类型,如果要得到真正的类型,需要转换为真正的类型。

4.2 ViewStub.setVisibility方法:

/**
 * When visibility is set to {@link #VISIBLE} or {@link #INVISIBLE},
 * {@link #inflate()} is invoked and this StubbedView is replaced in its parent
 * by the inflated layout resource. After that calls to this function are passed
 * through to the inflated view.
 *
 * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
 *
 * @see #inflate() 
 */
@Override
@android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
public void setVisibility(int visibility) {
    if (mInflatedViewRef != null) {
        View view = mInflatedViewRef.get();
        if (view != null) {
            view.setVisibility(visibility);
        } else {
            throw new IllegalStateException("setVisibility called on un-referenced view");
        }
    } else {
        super.setVisibility(visibility);
        if (visibility == VISIBLE || visibility == INVISIBLE) {
            inflate();
        }
    }
}

可以看出,mInflatedViewRef就是被装载的View。

刚开始时还没有装载,mInflatedViewRef为空,于是走到else分支,调用inflate方法进行加载。

已经装载过后,mInflatedViewRef不为空,走if分支,调用View.setVisibility。

因此,ViewStub.setVisibility本质上是对ViewStub.inflate和View.setVisibility的封装,方便对ViewStub显示和隐藏用的。

5 Demo使用

activity_view_stub_test
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ViewStub
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:id="@+id/view_stub_tv"
        android:inflatedId="@+id/tv_inflate"
        android:layout="@layout/view_stub_content_text_layout"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="32dp"
        android:text="显示"
        android:onClick="show"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/button5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:text="隐藏"
        android:onClick="hide"
        app:layout_constraintBottom_toTopOf="@+id/button4"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
view_stub_content_text_layout
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/tv_view_stub_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:text="这是 view stub 真正的内容">
</TextView>

主页面:

package imageviewtype

import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.ViewStub
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.example.timelinedemo.R


class ViewStubTestActivity : AppCompatActivity() {

    private var tvView: TextView? = null
    private var pvStub: ViewStub? = null

    private var button5: Button? = null //隐藏
    private var button4: Button? = null //显示

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_view_stub_test)
        button5 = findViewById(R.id.button5)
        button4 = findViewById(R.id.button4)

        button4?.setOnClickListener {
            show(it)
        }
        button5?.setOnClickListener {
            hide(it)
        }
    }

    fun show(view: View) {
        // ViewStub 只能 inflate 一次,因此这里判断只有在没装载时,才进行 inflate
        if (tvView == null) {
            // viewStub 的 ID: view_stub_tv
            Log.d("tanyonglin","View没有被装载过")
            tvView = findViewById<ViewStub>(R.id.view_stub_tv).inflate() as TextView
            // 得到被装载的 TextView,可以修改其文本
            tvView?.text = "你好啊"
        } else {
            Log.d("tanyonglin","View被装载了已经")
            tvView?.visibility = View.VISIBLE
        }
    }

    fun hide(view: View) {
        tvView?.visibility = View.GONE
    }
}

总结:ViewStub只能被inflate一次,因为inflate后,ViewStub就不存在了,被替换成真正的View。

下篇分析下ViewStub源码。