前言
在优化App性能方面,app的启动速度一定是被大家关注的一点。正好笔者也有自己的想法和总结,与大家一起分享。本篇就教大家如何提升app启动速度!
介绍
应用启动可以在三种状态中进行,每一种状态都会影响你的应用需要多长时间才能被用户看到:冷启动、温启动或热启动。在冷启动时,你的应用程序从头开始。在其他状态下,系统需要将正在运行的应用程序从后台带到前台。所以建议您以冷启动为基准作为优化,这样同时也兼顾了温启动和热启动
三种启动状态
冷:进程没有初始化,一切需要重头开始,比如第一次启动这个App
暖:暖启动包含了一些冷启动的操作但开销比热启动小,比如系统将你的应用程序从内存中移除,或用户退出你的App再启动。
热:热启动要比冷启动简单得多,开销也低得多。在热启动时,系统所做的就是将您的活动放到前台。如果应用程序的所有活动仍然驻留在内存中,那么该应用程序可以避免重复对象初始化、Inflat布局和呈现。
在冷启动开始时系统有三个任务
- 加载和启动应用程序
- 程序启动之后立刻显示一个空白界面
- 创建这个进程
一旦App进程创建成功,将会立刻进行下一阶段
执行
Application#onCreate
---> 创建MainActivity
--->Inflating views
--->draw UI
一旦应用程序完成第一次绘制,当前显示背景窗口将会替换成MainActivity,此时App的启动所用时间到此为止。减少启动时间,其实就是压缩窗口交换的时间。
优化
1.解决启动白屏
最好的方式是设置一个WindowBackground
主题
<style name="AppTheme.Launcher">
<item name="android:windowBackground">@mipmap/launch_bg</item>
</style>
<activity ...
android:theme="@style/AppTheme.Launcher" />
然后在MainActivity
里转回正常主题,在super.onCreate
和setcontentView
之前设置为正常主题
public class MyMainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);
// ...
}
}
(注意:如果最先启动的Activity为闪屏页,这一步可省略)
以上解决了白屏问题,但未真正的减少启动时间,下面教你如何压缩启动时间,下面这张图说明了冷启动过程中的重要部分(来源于Google )。
图中包括:
1.Applicaiton#onCreate
2.运行
MainThread
主线程3.初始化
Activity
,回调onCreate
,布局解析与绘制
2.Applicaiton优化
- 子线程初始化第三方库初始化
- 使用时初始化
2.1 优化重点onCreate
函数,不应该做耗时的操作,第三方jar包初始化放到子线程中,可以利用IntentService
,Thread
等异步任务。此处也有需要斟酌的地方,比如可能会出现WorkThread
中尚未初始化完毕但MainThread
中已经使用的错误,这种情况还是建议放在onCreate
方法这个主线程中初始化
2.2其次,项目中弱应用,不会被常用的第三方库可以延迟到使用时才初始化,下面我举个例子。
就拿网易云信IM即时通信来说,很多APP的IM都是弱应用,可能不会被使用到,但是这个初始化又是一个相对比较沉重的任务,所以可以放到使用时初始化,下面放一张初始化SDK说明
可以看出网易的IM文档还是非常人性化的提供了在任意位置初始化,但是只有文字表述,没有代码实例,初一看可能不太明白,我将代码实现表示如下:
1.在Application中
仅仅配置IM,不影响性能
@Override
public void onCreate() {
super.onCreate();
// 在Application中仅仅配置网易云信IM
buildConfigNim();
}
private void buildConfigNim() {
DemoCache.setContext(this);
NIMClient.config(this, null, NimSDKOptionConfig.getSDKOptions(this));
}
2.在需要用到时初始化,可以重复调用内部做了判断
/**
* 动态初始化
*/
private void initIM() {
// 在使用时调用这行代码进行初始化,可以重复调用,内部做了判断
NIMClient.initSDK();
// 初始化UIKit模块
initUIKit();
}
这是一种很好的思想,不仅局限于如此,日常开发中我们也是如此,比如
new
个集合有时候也是在使用的时候new
。做到有开销有使用,无开销就消掉。好了回到优化,我再举几个例子
举例子
常规操作异步线程初始化第三方库
public class BaseApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
new WorkThread().start();
}
public class WorkThread extends Thread{
@Override
public void run() {
// 设置比正常线程稍微低一点的线程优先级
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// 异步初始化第三方库
// init...
}
}
}
轻轻简述线程优先级相关概念:Android
分为UI
线程和background
线程,UI
线程中开启一个子线程默认分配到Default
组跟UI
线程同级,平分抢占CPU
资源。为避免这种情况可设置线程优先级,式情况而定。下面为常见的两种
// 稍微低于正常线程,利于界面的响应
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
// 低优先级
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
如果你在项目里面使用了RxJava你也可以这样,看代码注释
public class BaseApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
initOfMain();
initOfWork();
init();
}
public void init(){
// 必须在onCreate中执行的初始化
}
private void initOfMain() {
// 开启一个线程 不会等此函数执行完再执行下一个函数
Completable.fromAction(() -> {
// initSDK...
// 指定为主线程
}).subscribeOn(AndroidSchedulers.mainThread()).subscribe();
}
private void initOfWork(){
// 开启一个线程 不会等此函数执行完再执行下一个函数
Completable.fromAction(() -> {
// initSDK...
// 在子线程中执行
}).subscribeOn(Schedulers.io()).subscribe();
}
}
注意:#initOfMain
和#initOfWork
里面的任务有可能会进入Activity
才执行完毕,这要根据线程耗时的多少和界面跳转时机来
新思路
大多是App都会有一个 SplashActivity
做广告展示,延迟2秒再进入首页。期间做一些简单的初始化,或者MainActivity
的数据预加载。
想一想能否在Splash展示的同时MainActivity的View也同时加载,业务逻辑也同步进行可不可以呢。
-
方案
将SplashActivity
拿掉,改用SplashFragment
挂载到MainActivity
上,逻辑不变只是把继承对象改为Fragment
同时把应用启动入口改为MainActivity
先显示SplashFragment
,等它展示完了再remove
代码如下
public class MainActivity extends AppCompatActivity {
private MyHandle mHandle = new MyHandle(this);
private ProgressBar progressBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ViewStub mainLayout = findViewById(R.id.main_container);
SplashFragment splashFragment = new SplashFragment();
FragmentManager supportFragmentManager = getSupportFragmentManager();
if (supportFragmentManager != null) {
FragmentTransaction transaction = supportFragmentManager.beginTransaction();
if (transaction != null) {
transaction.replace(R.id.splash_container, splashFragment).commitAllowingStateLoss();
}
}
// 2. 执行主页的网络操作
new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(3000);
mHandle.sendEmptyMessage(0);
}
}).start();
// 3. 渲染完毕加载主界面
getWindow().getDecorView().post(new Runnable() {
@Override
public void run() {
View mainView = mainLayout.inflate();
initView(mainView);
}
});
// 4.延迟 remove splashFragment
getWindow().getDecorView().post(new Runnable() {
@Override
public void run() {
mHandle.postDelayed(new DelayRunnableImpl(MainActivity.this, splashFragment), 200);
}
});
}
/**
* 初始化主页View
*
* @param mainView
*/
private void initView(View mainView) {
if (mainView != null) {
progressBar = findViewById(R.id.progress);
progressBar.setVisibility(View.VISIBLE);
}
}
private static class MyHandle extends Handler {
private WeakReference<MainActivity> wRef;
public MyHandle(MainActivity activity) {
this.wRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity mainActivity = wRef.get();
if (mainActivity != null) {
mainActivity.progressBar.setVisibility(View.GONE);
}
}
}
private class DelayRunnableImpl implements Runnable {
WeakReference<Context> contextWref;
WeakReference<Fragment> fragmentWref;
public DelayRunnableImpl(Context contextWref, Fragment fragmentWref) {
this.contextWref = new WeakReference<>(contextWref);
this.fragmentWref = new WeakReference<>(fragmentWref);
}
@Override
public void run() {
FragmentActivity context = (FragmentActivity) contextWref.get();
if (context != null) {
FragmentManager supportFragmentManager = context.getSupportFragmentManager();
if (supportFragmentManager != null) {
FragmentTransaction fragmentTransaction = supportFragmentManager.beginTransaction();
SplashFragment fragment = (SplashFragment) fragmentWref.get();
if (fragment != null) {
fragmentTransaction.remove(fragment).commitAllowingStateLoss();
}
}
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if(mHandle != null){
mHandle.removeCallbacksAndMessages(null);
}
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
tools:context=".MainActivity">
<ViewStub
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/main_content" />
<!-- 启动页容器 -->
<FrameLayout
android:id="@+id/splash_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>
SplashFragment
public class SplashFragment extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_splash, null);
initView(view);
return view;
}
private void initView(View view) {
if(view!=null){
PropertyValuesHolder pvh = PropertyValuesHolder.ofFloat("alpha",1f,0.7f,0);
ObjectAnimator.ofPropertyValuesHolder(view,pvh).setDuration(2000).start();
}
}
}
采用以上方式发现APP
启动明显加快。不过要提醒一下一点是,使用这种方式时 如果MainActivity
启动模式为singleTask
,会出现这种现象 我们从二级界面按Home
键再回来时,界面显示的是一级界面。你可以将入口的启动模式去掉来保留界面依然停留在二级界面
3.首次出现Activity优化
- 避免过度绘制
去掉不必要的Background
属性,如果设置了WindowBackground Style
属性在最外层布局文件中切莫使用相同的Background
,我的建议是不使用WindowBackground
。LinearLayout
相对于RelativeLayout
地Measure
效率要高。可以使用ConstraintLayout
来减少布局层级,而且它还有很多你必须使用代码来实现的API。少使用View.GONE
属性。尽量使用ViewStub
标签当我们使用的时候Inflate和实例化 - 减少阻塞任务
什么是阻塞任务,比如SharedPreference
的Commit
函数会阻塞IO
,这个函数虽然执行很快,但是系统会有另外一个线程来负责写操作,当apply频率高的时候,该线程就会比较占用CPU
资源 。还有文件读写操作,特别是读取本地的大文件,这里面涉及到磁盘读取和数据流转换都是比较耗时的。 最好放到子线程中执行,避免阻塞 UI 线程,但也要适当的控制好线程数量
之前提到过一旦应用程序完成第一次绘制,当前显示背景窗口将会替换成MainActivity
,此时App
的启动所用时间到此为止。
4.如何知晓启动时长
1.通过筛选log日志Displayed
2.使用adb命令
adb shell am start -W packageName/packageName.MainActivity
总结
以上就是我分享的 APP 启动优化,有不足之处还望指点。
参考文件:https://developer.android.google.cn/topic/performance/vitals/launch-time