zoukankan      html  css  js  c++  java
  • 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线程),那么此时只要不在子线程的主线程上做耗时操作,那么就可以使得子进程可以像主进程一样响应用户。

  • 相关阅读:
    Using System Partitioning
    startup命令
    [转]Oracle 10g/11g 密码策略 用户口令 大小写敏感
    [转]Oracle DB 加强的数据安全管理
    Scheduling Jobs with Oracle Scheduler
    [转]Oracle DB SQL 计划管理
    Performing Time-Based or Change-Based Incomplete Recovery
    闪回数据归档
    Managing Optimizer Statistics
    性能测试--十个命令迅速发现性能问题
  • 原文地址:https://www.cnblogs.com/muouren/p/11741309.html
Copyright © 2011-2022 走看看