参考
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 的处理方式有三种:
- 使用系统默认的 StartingWindow :用户点了应用图标启动应用,马上弹出系统默认的 StartingWindow(就是做动画的那个 Window) ,等应用加载好第一帧之后,StartingWindow 消失,显示应用第一帧,无缝衔接,体验还不错,这也是通常大部分 Android 应用的场景;比如大部分 Android 系统的自带应用,即刻、汽车之家等
- 自己定制简单的 StartingWindow :用户点了应用图标启动应用,弹出应用自己定制的StartingWindow,等应用加载好第一帧之后,定制的 StartingWindow 消失,显示应用主界面,由于 StartingWindow 是自己定制的,启动的时候 Decode Bitmap 或者 Inflate 自定义 Layout 会有一定的耗时,但是总的来说与系统默认的差别不大,用户体验优;这样的应用包括淘宝、京东、微博、今日头条、美团等
- 把 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
应用进程不存在的情况下,从点击桌面应用图标,到应用启动(冷启动),大概会经历以下流程:
- Launcher startActivity
- AMS startActivity
- Zygote fork 进程
- ActivityThread main()
- ActivityThread.attach
- handleBindApplication
- Application.attachBaseContext
- ContentProvider.installContentProviders
- Application.onCreate
- ActivityThread 进入loop循环
- 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线程),那么此时只要不在子线程的主线程上做耗时操作,那么就可以使得子进程可以像主进程一样响应用户。
原文地址:https://www.cnblogs.com/muouren/p/11741309.html