APP启动时白屏优化及multidex优化

Posted 嘤嘤嘤123

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了APP启动时白屏优化及multidex优化相关的知识,希望对你有一定的参考价值。

参考

https://juejin.im/post/5d95f4a4f265da5b8f10714b

https://blog.csdn.net/suyimin2010/article/details/80635579

https://www.cnblogs.com/whycxb/p/9312914.html

 

问题说明

当打开一个Activity时,如果这个Activity所属Application还没有在运行,系统会为这个Activity的创建一个进程(每开启一个进程都会有一个Application,所以Application的onCreate()可能会被调用多次),但进程的创建与初始化都需要时间,在这个动作完成之前,如果初始化的时间过长,屏幕上可能没有任何动静,用户会以为没有点到按钮。所以既不能停在原来的地方又没到显示新的界面,怎么办呢?这就有了StartingWindow(也称之为PreviewWindow)的出现,这样看起来就像Activity已经启动起来了,只是数据内容还没有初始化好。

StartingWindow一般出现在应用程序进程创建并初始化成功前,所以它是个临时窗口,对应的WindowType是TYPE_APPLICATION_STARTING。目的是告诉用户,系统已经接受到操作,正在响应,在程序初始化完成后显示目的UI,同时移除这个窗口。

这个StartingWindow就是我们要讨论的白屏和黑屏的"元凶"。

 

设置Theme

怎么解决呢?

市面上的常用app的StartingWindow 的处理方式有三种:

  1. 使用系统默认的 StartingWindow :用户点了应用图标启动应用,马上弹出系统默认的 StartingWindow(就是做动画的那个 Window) ,等应用加载好第一帧之后,StartingWindow 消失,显示应用第一帧,无缝衔接,体验还不错,这也是通常大部分 android 应用的场景;比如大部分 Android 系统的自带应用,即刻、汽车之家等

 

  1. 自己定制简单的 StartingWindow :用户点了应用图标启动应用,弹出应用自己定制的StartingWindow,等应用加载好第一帧之后,定制的 StartingWindow 消失,显示应用主界面,由于 StartingWindow 是自己定制的,启动的时候 Decode Bitmap 或者 Inflate 自定义 Layout 会有一定的耗时,但是总的来说与系统默认的差别不大,用户体验优;这样的应用包括淘宝、京东、微博、今日头条、美团等

 

  1. 把 StartingWindow 禁掉或者设置透明 :用户点了应用图标启动应用,由于 StartingWindow 被禁掉或者被设置透明,所以会出现点击图标后,除了图标黑一下之外没有任何响应,过个 1-N 秒(取决于应用第一帧的加载速度),直接显示应用主界面。这样的毒瘤应用包括:微信、微信读书、UC 浏览器、支付宝、工商银行、米家等。

 

一般我们使用第二种处理方式:

我们会对Application和Activity设置Theme,系统会根据设置的Theme初始化StartingWindow。

Window布局的顶层是DecorView,StartingWindow显示一个空DecorView,但是会给这个DecorView应用要到开的Activity指定的Theme,如果这个Activity没有指定Theme就用Application的(Application系统要求必须设置Theme)。

 

首先创建一个应用启动页(StartingWindow)的theme

<style name="AppTheme.StartingWindowTheme">
    <!-- 可以设置成纯颜色(设置一个和Activity UI相似的背景) -->
    <!--<item name="android:windowBackground">@color/startingwindow_bgcolor</item>-->
     <!--也可以设置成一张图片 -->
    <item name="android:windowBackground">@drawable/startingwindow_bg</item>
</style>

 

在主Activity上应用上边创建的theme

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.why.project.androidstartingwindowdemo">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <!--将首页的them设置成自定义的样式-->
        <activity android:name=".MainActivity"
                  android:theme="@style/AppTheme.StartingWindowTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>
</manifest>

 

在主activity启动后恢复原有的theme

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setTheme(R.style.AppTheme);//恢复原有的样式
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    ...
}

 还有一种效果是 主activity的theme不设置其他theme,然后主activity布局文件中将背景(android:background)设置为透明,那么这样就可以实现APP启动后StartingWindow和 主activity 是同一张背景图片。

 

MultiDex 的优化

https://juejin.im/post/5d95f4a4f265da5b8f10714b#heading-10

 

看这个之前需要线了解一下multidex的原理,在另一个文档里。

 

当需要多dex支持时,需要使用到multidex的support库,但这个库在4.4的机器上第一次启动时比较耗时的(5.0及以上都默认支持多dex),所以这个是另一个白屏/黑屏问题的原因。

在Android 4.4的机器打印MultiDex.install(context)耗时如下:

MultiDex.install 耗时:1320

 

应用进程不存在的情况下,从点击桌面应用图标,到应用启动(冷启动),大概会经历以下流程:

  1. Launcher startActivity
  2. AMS startActivity
  3. Zygote fork 进程
  4. ActivityThread main()
    1. ActivityThread.attach
    2. handleBindApplication
    3. Application.attachBaseContext
    4. ContentProvider.installContentProviders
    5. Application.onCreate
  5. ActivityThread 进入loop循环
  6. Activity生命周期回调,onCreate、onStart、onResume...

那么优化逻辑如下:
1. 创建临时文件,作为判断MultiDex是否加载完的条件
2. 启动LoadDexActivity去加载MultiDex(LoadDexActivity在单独进程),加载完会删除临时文件
3. 开启while循环,直到临时文件不存在才跳出循环,进入Application的onCreate

下边具体代码:

MyApplication

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);

    boolean isMainProcess = isMainProcess(base);

    //主进程并且vm不支持多dex的情况下才使用 MultiDex
    if (isMainProcess && !SystemUtil.isVMMultidexCapable()){
        //在里边如果安装multidex,就会有个while循环来检查,此时主进程就卡在 此处。
        loadMultiDex(base);
    }
}
private void loadMultiDex(Context context) {
    newTempFile(context); //创建临时文件

    //启动另一个进程去加载MultiDex,LoadMultiDexActivity在清单文件中设置了另一个进程。
    Intent intent = new Intent(context, LoadMultiDexActivity.class);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    context.startActivity(intent);

    //检查刚创建的newTempFile,如果不存在那么表示MultiDex安装完(安装完会删除临时文件)
    checkUntilLoadDexSuccess(context);

    //另一个进程以及加载 MultiDex,有缓存了,所以主进程再加载就很快了。
    //第二次MultiDex.install, 为什么主进程要再加载,因为每个进程都有一个ClassLoader
    MultiDex.install(context);

    preNewActivity();
}

对象第一次创建的时候,java虚拟机首先检查类对应的Class 对象是否已经加载。
如果没有加载,jvm会根据类名查找.class文件,将其Class对象载入。
同一个类第二次new的时候就不需要加载类对象,而是直接实例化,创建时间就缩短了。
private void preNewActivity() {
    long startTime = System.currentTimeMillis();
    MainActivity mainActivity = new MainActivity();
    Log.d(TAG, "preNewActivity 耗时: " + (System.currentTimeMillis() - startTime));
}

//创建一个临时文件,MultiDex install 成功后删除
private void newTempFile(Context context) {
    try {
        File file = new File(context.getCacheDir().getAbsolutePath(), "load_dex.tmp");
        if (!file.exists()) {
            Log.d(TAG, "newTempFile: ");
            file.createNewFile();
        }
    } catch (Throwable th) {
        th.printStackTrace();
    }
}

/**
 * 检查MultiDex是否安装完,通过判断临时文件是否被删除,此方法里导致主进程在这里边卡住,不进行执行。
 * @param context
 * @return
 */
private void checkUntilLoadDexSuccess(Context context) {
    File file = new File(context.getCacheDir().getAbsolutePath(), "load_dex.tmp");
    int i = 0;
    int waitTime = 100; //睡眠时间
    try {
        while (file.exists()) {
            Thread.sleep(waitTime);
            Log.d(TAG, "checkUntilLoadDexSuccess: sleep count = " + ++i);
            if (i > 40) {
                Log.d(TAG, "checkUntilLoadDexSuccess: 超时,等待时间: " + (waitTime * i));
                break;
            }
        }
    }catch (Exception e){
        e.printStackTrace();
    }

}

 

接着上边启动LoadMultiDexActivity,此activity组件是运行在子进程中的。

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


    Thread thread = new Thread() {
        @Override
        public void run() {

            loadMultiDex();
        }
    };
    thread.setName("multi_dex");
    thread.start();
   
    // 显示一个dialog
    showLoadingDialog();
}


private void loadMultiDex(){

    MultiDex.install(LoadMultiDexActivity.this);

    try {
        //模拟MultiDex耗时很久的情况
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }


    aftetMultiDex();
}

private void aftetMultiDex() {

    deleteTempFile(this);

    //将这个进程杀死
    Log.d(TAG, "aftetMultiDex: ");
    finish();
    Process.killProcess(Process.myPid());
}

private void deleteTempFile(Context context) {
    try {
        File file = new File(context.getCacheDir().getAbsolutePath(), "load_dex.tmp");
        if (file.exists()) {
            file.delete();
            Log.d(TAG, "deleteTempFile: ");
        }
    } catch (Throwable th) {
        th.printStackTrace();
    }
}

private void showLoadingDialog(){
    new AlertDialog.Builder(this)
            .setMessage("加载中,请稍后...")
            .show();
}

 

当子进程处理完成后主进程attachBaseContext就会继续往下执行。

其实这里便做了两个优化,

  • 一个就是multidex的加载放在子进程中,
  • 还有一个就是提前对主activity进行创建,静态初始化。

 

主线程长时间循环检测文件时,为什么不会卡?

是因为主进程的主线程确实卡在检查文件处,但因为启动了子进程,而子进程也有自己的主线程(ui线程),那么此时只要不在子线程的主线程上做耗时操作,那么就可以使得子进程可以像主进程一样响应用户。

 

以上是关于APP启动时白屏优化及multidex优化的主要内容,如果未能解决你的问题,请参考以下文章

解决 优化 app 启动页 白屏 黑屏问题

解决 优化 app 启动页 白屏 黑屏问题

通过设置Activity的Theme来优化App启动白屏问题

Android应用启动时白屏或者黑屏处理办法

Android MVVM框架使用(十三)UI更新 (App启动白屏优化适配Android10.0深色模式)

Android项目实战(四十二):启动页优化,去除短暂白屏或黑屏