性能优化(1.3)-APP启动速度优化实例解析

主目录见:Android高级进阶知识(这是总目录索引)
 这篇是看了别人的想法之后,发现这个方法还是蛮好的,所以在这里给大家一起分享下,毕竟性能还是大家所追求的。而且有时候性能优化就是一个简单的举动也许就能改变很多,使你的App看过去更专业。上一篇我们讲了[布局优化(扁平化,Merge的使用,ViewStub的使用)],这一篇也会用到这里的知识。当然如果你想要这篇文章的例子可以[点击下载]

一.目标

今天分享这一篇呢主要就是给大家提供一个方案吧,也希望大家如果有好的东西可以分享出来给大家知道。今天的目标是:
1.优化App的冷启动速度;
2.同时从性能上说说白屏的解决方法优劣。

二.优化实例解析

我们都知道,Activity的启动流程对于我们的来说,能优化的流程里面只有后面View怎么呈现出来,View渲染完毕数据的请求加载。按理说这部分能做的工作最多。我们先来说说View是怎么显示在界面上的?

Android的视图都是通过window来呈现的,不管Activity,dialog或者Toast等都有一个对应的window,然后通过WindowManager来管理View。而且在Acitivity启动过程中又会通过WindowManagerImpl将window和view(DecorView)进行关联调用addView将decorView添加。接着从ViewRootImpl调用performTraversals()依次经过measure(),layout()和draw()三个过程将view显示出来。

从这上面可以可以看出有一大部分的时间占用在视图的渲染上面。我们先来看看以前早期我们的启动页设计:

  • 我们首先会先创建一个SplashActivity页面,然后在这里写启动页的展示信息而且也会有几秒跳过广告等,同时这里可能做一些数据的预加载,然后数据加载完毕传给MainActivity。

然后我们看看我们这篇的做法:

  • 我们这里把SplashAcitivity改成Fragment的形式实现,然后在MainActivity的时候进行展示这个Fragment,在展示这个Fragment的同时,我们在窗体加载完毕之后直接把我们真正的布局加载进来,这个布局可能会比较复杂,所以我们在展示Fragment的时候我们不渲染,而是用ViewStub进行懒加载,与此同时,我们还可以同时进行异步网络数据的加载。

从上面的做法我们可以看出,第一种做法,虽然SplashActivity加载很快,然后数据也做了预加载,但是到MainActivity的时候,首页布局依旧是比较复杂的所以还是会经过view的measure,layout,draw整个过程,还是拖慢了整个启动过程,而第二个方案却将启动页和MainActivity主布局合并在一起,而且在渲染Splash页面的时候,MainActivity的主布局是放在ViewStub里面的。同时数据可以在Splash页面展示的时候就进行加载,用户感知不到。

1.SplashFragment

首先我们来从启动页Fragment说起,这个页面我们尽量简单。这里我们先来看看xml内容:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
   >

    <FrameLayout
        android:id="@+id/frame"
        android:background="@mipmap/bg_welcome"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </FrameLayout>

</RelativeLayout>

我们看到这里只是简单的一个背景而已,当然有的app会在这边播放一个视频,或者在这边加载一个广告。但是这是具体的设计决定,我们不参与细讲。我们来看看我们的splashFragment:

public class SplashFragment extends Fragment {
    @Override
    @Nullable
    public View onCreateView(LayoutInflater inflater,
            @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_splash, container,false);
    }
}

我们看到我们这里只是简单地将我们的布局inflate进来。然后我们就将这个fragment在我们的MainAcitivity里面展示。

2.MainActivity

我们这里先来看看MainActivity里面setContentView的布局:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                >
    <ViewStub
        android:id="@+id/content_viewstub"
        android:layout="@layout/activity_main_viewstub"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <FrameLayout
        android:id="@+id/frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </FrameLayout>
</RelativeLayout>

我们看到第一个ViewStub是用于放我们MainActivity主布局activity_main_viewstub的,第二个FrameLayout是为了放我们SplashFragment的。我们在刚开始加载的时候ViewStub是不会加载到我们的view树中的,所以不占用我们的渲染时间。我们看下onCreate里面做了什么:

  @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        splashFragment = new SplashFragment();
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        transaction.replace(R.id.frame, splashFragment);
        transaction.commit();

        viewStub = (ViewStub)findViewById(R.id.content_viewstub);
        //1.判断当窗体加载完毕的时候,就把我们真正的布局加载进来
        getWindow().getDecorView().post(new Runnable() {
            @Override
            public void run() {
                // 开启延迟加载
                mHandler.post(new Runnable() {

                    @Override
                    public void run() {
                        //将viewstub加载进来
                        viewStub.inflate();
                    }
                } );
            }
        });
        //2.判断当窗体加载完毕的时候执行,延迟一段时间是为了模拟做动画的耗时。
        getWindow().getDecorView().post(new Runnable() {

            @Override
            public void run() {
                // 开启延迟加载,也可以不用延迟可以立马执行(我这里延迟是为了实现fragment里面的动画效果的耗时)
                mHandler.postDelayed(new DelayRunnable(MainActivity.this, splashFragment) ,2000);
            }
        });
        //3.同时进行异步加载数据
    }

我们看到程序刚进来就将fragment显示出来,然后在窗体加载完毕才将我们的真正布局加载进来,同时模拟了做动画或者广告时间,当然实际过程中可以在动画完毕或者广告结束再去移除Fragment,或者我们有跳过广告的时候我们可以做一个回调给MainActivity让他马上加载主页布局。当然同时我们可以加载我们的网络数据。接着我们来看我们移除Fragment的代码:

  static class DelayRunnable implements Runnable{
        private WeakReference<Context> contextRef;
        private WeakReference<SplashFragment> fragmentRef;

        public DelayRunnable(Context context, SplashFragment f) {
            contextRef = new WeakReference<>(context);
            fragmentRef = new WeakReference<>(f);
        }

        @Override
        public void run() {
            if(contextRef!=null){
                SplashFragment splashFragment = fragmentRef.get();
                if(splashFragment==null){
                    return;
                }
                FragmentActivity activity = (FragmentActivity) contextRef.get();
                FragmentTransaction transaction = activity.getSupportFragmentManager().beginTransaction();
                transaction.remove(splashFragment);
                transaction.commit();
            }
        }
    }

这里说下为什么要用弱引用来存放着两个对象,我们知道,我们在使用Handler或者Timer的时候,如果是static的类且有时间延迟的话那么就有可能内存溢出,因为static的生存时间周期可能长于Activity,这样会导致Activity关闭了,但是引用还是被static类持有,GC不能回收。所以这里用弱应用,也就是说如果Activity关闭,那么GC可以随时回收这个Activity。最后我们看下效果:

《性能优化(1.3)-APP启动速度优化实例解析》 splash.gif

我们看到我们的效果跟以前用splashActivity在视觉上没什么区别,因为我们的启动页面布局都比较简单:
我们可以用指令来进行对比:

adb shell am start -W com.lenovohit.splashoptimize/com.lenovohit.splashoptimize.MainActivity

《性能优化(1.3)-APP启动速度优化实例解析》 启动时间

3.白屏或者黑屏问题

其实这里的白屏和黑屏都是同个问题(只是主题的原因有可能会不同),是因为window的背景,因为DecorView默认会有一个纯色的背景,由于我们的activity可能做得工作太多,或者application初始化太多东西,我们的实际视图还没渲染完成,所以我们启动我们Activity会先显示window的背景。所以由于设备的性能原因表现出来的白屏或者黑屏问题也不一样。一般来说这有两种方法解决:
1.在style文件中加入如下代码:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    ......
    <item name="android:windowIsTranslucent">true</item>
    <item name="android:windowNoTitle">true</item>
</style>

2.给Window加上背景避免白屏

1.
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 底层白色 -->
    <item android:drawable="@color/white" />

    <!-- 顶层Logo居中 -->
    <item>
        <bitmap
            android:gravity="center"
            android:src="@drawable/ic_github" />
    </item>
</layer-list>

2.
<style name="SplashTheme" parent="AppTheme">
        <item name="android:windowBackground">@drawable/bg_splash</item>
</style>

3.
<activity
            android:name=".activity.MainActivity"
            android:screenOrientation="portrait"
            android:theme="@style/SplashTheme">

我们看到这上面两种方式都是会解决白屏问题,但是我还是想说一下我个人的看法,我们知道,虽然第一种方法让主题变成透明了,但是其实android系统还是回去渲染,显然这也可能影响过度绘制的,第二种方法也是类似,因为我们动态给window加上了背景,相当于我们自动多了一层背景,如果我们activity里面的布局也有背景的话,那么也是会影响过度绘制的。所以我觉得最好的方式就是欢迎页尽量简单,能迅速渲染出我们的布局,这样的话白屏的可能性也就变小了。

总结:今天讲的只是一个例子,大家可以当做一个思维的拓展,还有最后的白屏黑屏问题如果大家有自己的想法欢迎留言呀,希望我们共同进步哈。

    原文作者:ZJ_Rocky
    原文地址: https://www.jianshu.com/p/bce1c6782f37
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞