1. 利用启动的形式
在Android中,利用启动个别可分为三种:冷启动、温启动、热启动。
那么什么是冷启动、温启动和热启动呢?上面咱们来简略看一下它们的定义:
- 冷启动:当启动利用时,后盾没有该利用的过程。这时零碎会又一次创立一个新的过程调配给该利用,这个启动形式就是冷启动。
- 温启动:当启动利用时,后盾已有该利用的过程,然而Activity可能因为内存不足被回收。这样零碎会从已有的过程中来启动这个Activity,这个启动形式叫温启动。
- 热启动:当启动利用时,后盾已有该利用的过程,且Activity依然存在内存中没有被回收。这样零碎间接把这个Activity拉到前台即可,这个启动形式叫热启动。
因为冷启动绝对于其余启动形式多了过程的创立(Zygote过程fork创立过程)以及利用的资源加载和初始化(Application的创立及初始化),所以相对来说会比拟耗时,所以咱们个别说的App启动优化个别指的都是App的冷启动优化。
2. 优化计划
App启动优化的实质就是:启动速度和体验的优化。
这就好比早些年你去饭店吃饭,你想要点餐但等了半天都没有服务人员过去,可能就等得不耐烦间接走开了。同样的,对于APP来说,如果用户点击App后长时间都打不开,用户就很可能失去急躁而卸载利用。
所以启动速度是用户对咱们App的第一体验。如果启动速度过慢,用户第一印象就会很差,这样即便你性能做出花来,用户也不会违心去应用。
其实要想优化App的启动体验,要害就是要让用户更快地获取到利用的内容(晦涩,不卡顿、不期待),那么咱们应该怎么做呢?
2.1 案例剖析
这里咱们还是先以之前说的去饭店吃饭为例来展开讨论。当初的饭店竞争是尤为得强烈,为了可能进步顾客的体验、留住顾客,真的是使出了浑身解数,除了口味之外,服务品质也是被摆在了越来越重要的地位。
就比方说:
- 为了可能更快地给用户提供点餐服务,当初的饭店每个座子上根本都贴上了点餐的二维码。
- 一些热门须要排队的餐馆,在前台张贴了二维码,提供排队取号和提前点餐服务。
- 一些长年爆满须要排队的餐馆,还提供了零食、茶水、棋牌以及美甲等服务。
- 有些餐厅为了进步上菜的速度和体验,设置了倒计时免单的服务。
以上的改良措施,都是这些年餐饮行业在竞争下,一直进步服务质量的产物。能够说谁的服务质量落下了,谁就有可能被淘汰。
其实咱们仔细分析一下下面所列举的,不难看出有很多是能够让咱们借鉴的。
(1)案例1
剖析:饭店提供了点餐的二维码,实质上是将一件被动期待转变为被动申请的一种过程,主观上缩小了期待的工夫,从而进步了速度。
类比:这对应咱们的应用程序,就像原先一些耗时不必要的三方库须要被动期待其初始化结束程序才会持续进行,转变为先不初始化这部分耗时的三方库,等真正用到时再进行初始化;又相似咱们应用程序的游客模式,无需被迫进行一堆简单的用户注册过程,就能够间接进入程序应用,待波及一些用户信息性能的时候再提醒用户注册。
(2)案例2
剖析:热门餐馆同时提供排队取号和提前点餐服务,实质上是将原先无奈同时进行的操作(须要先排上队等到座位,能力扫描座位号点餐)变成了可同时进行的操作,将串行工作转化为并行任务,从而节俭了工夫。
类比:这对应咱们的应用程序,就是将一些本来在主线程串行执行的耗时资源/数据加载,改为在子线程中并发执行。这在几个耗时工作耗时差距不大的时候优化尤为显著。
(3)案例3
剖析:在顾客排队期待的时候,提供零食、茶水、棋牌以及美甲等服务,实质上就是让顾客提前享受本餐馆的服务,从而缓解顾客期待的焦虑。
类比:这对应咱们的应用程序,就是开屏启动页。在Android12上,Google强制减少了这个开屏页,就是为了让用户提前看到你的利用页面,让用户产生利用启动很晦涩的假象,从而进步用户的启动体验。
(4)案例4
剖析:设置倒计时免单服务,实质上就是给期待加上了进度条下限以及超时抵偿。俗话说最让人胆怯的是期待,比期待更让人胆怯的是看不见止境的期待。而为期待设置下限,能够极大地缓解顾客期待的焦虑,毕竟期待超时了也是会有弥补的。
类比:这对应咱们的应用程序,就是一些利用(比方游戏)首次启动会十分耗时,所以它们通常会在启动页减少一个初始化/加载进度条页面,来通知用户啥时候能加载完,而不是无止境未知的期待。
通过剖析,咱们能够看到(1)、(2)两种操作是从技术的角度来实现的优化,而(3)、(4)两种操作则更多的是从业务的角度去实现的优化。
2.2 优化策略
从下面的案例剖析,咱们能够得出,利用启动优化,咱们能够别离从技术和业务的角度来进行决策。
2.2.1 技术优化
(1)针对启动流程工作进行梳理。
- 无耗时工作 -> 主线程
- 耗时且须要同步工作 -> 异步线程 + 同步锁
- 耗时且无需同步工作 -> 异步线程
(2)非必要不执行。
- 非必要数据 -> 懒加载
- 非必要工作 -> 提早/闲暇执行
- 非必要界面/布局 -> 提早加载
- 非必要性能 -> 删除/插件化
(3)数据结构优化,减小初始化工夫。
- 数据结构尽可能复用,防止内存抖动
- 数据结构申请的空间要恰到好处,不能过大(占用空间,创立慢)也不能过小(频繁裁减,效率低)
- 对于必要且量大的数据,可采取分段加载。
- 重要的资源本地缓存
2.2.2 业务优化
(1)业务流程整合。
- 多个相干的串行业务整合为对立的一个业务。
- 不相干的串行业务整合为并行的业务。
(2)业务流程拆分调整。
- 对业务进行拆分,拆分出次要(必要)业务和主要(非必要)业务。
- 别离对次要业务和主要业务进行优先级评估,业务执行按优先级从高到底顺次执行。
2.3 优化方向
在优化之前,让咱们先来剖析一下冷启动的过程:
Zygote创立利用过程 -> AMS申请ApplicationThread -> Application创立 -> attachBaseContext > onCreate -> ActivityThread启动Activity -> Activity生命周期(创立、布局加载、屏幕安排、首帧绘制)
以上过程,只有Application和Activity的生命周期这两个阶段对咱们来说是可控的,所以这就是咱们的优化方向。
3. 优化措施
3.1 启动流程优化
1.根据之前咱们列举的技术优化策略,首先须要对启动的所有工作流程进行梳理,而后对其执行形式进行优化。
- 只有必要且非耗时的工作在
主线程
执行。 - 耗时且须要同步的工作,应用
异步线程 + 同步锁
的形式执行。 - 耗时且无需同步的工作在
异步线程
执行。
2.第三方SDK初始化优化。
- 对于那些在启动时非必要的第三方SDK,能够提早初始化。
- 对于初始化耗时的第三方SDK,能够开启一个后盾服务/异步线程进行初始化。
3.应用工作执行框架。
这里咱们还能够应用一些第三方的工作启动框架,对启动流程进行优化。上面我就拿我开源的XTask 简略介绍一下:
这里咱们模仿了三种类型的工作:
- 1.优先级最重要的工作,执行工夫50ms;
- 2.独自的工作,没有执行上的先后顺序,然而须要同步,每个执行200ms;
- 3.耗时、最不重要的工作,等所有工作执行结束后执行,每个执行须要1000ms。
优化前
/** * 优化前的写法, 这里仅是演示模仿,理论的可能更简单 */private void doJobBeforeImprove(long startTime) { new TopPriorityJob(logger).doJob(); for (int i = 0; i < 4; i++) { new SingleJob((i + 1), logger).doJob(); } new LongTimeJob(logger).doJob(); log("工作执行结束,总共耗时:" + (System.currentTimeMillis() - startTime) + "ms");}
执行后果:
因为所有的工作都是执行在主线程,串行执行,所以花了大概1865ms。
优化后
/** * 优化后的写法, 这里仅是演示模仿,理论的可能更简单 */private void doJobAfterImprove(final long startTime) { ConcurrentGroupTaskStep groupTaskStep = XTask.getConcurrentGroupTask(); for (int i = 0; i < 4; i++) { groupTaskStep.addTask(buildSingleTask(i)); } XTask.getTaskChain() .addTask(new MainInitTask(logger)) .addTask(groupTaskStep) .addTask(new AsyncInitTask(logger)) .setTaskChainCallback(new TaskChainCallbackAdapter() { @Override public void onTaskChainCompleted(@NonNull ITaskChainEngine engine, @NonNull ITaskResult result) { log("工作齐全执行结束,总共耗时:" + (System.currentTimeMillis() - startTime) + "ms"); } }).start(); log("主线程工作执行结束,总共耗时:" + (System.currentTimeMillis() - startTime) + "ms");}@NonNullprivate XTaskStep buildSingleTask(int finalI) { return XTask.getTask(new TaskCommand() { @Override public void run() throws Exception { new SingleJob((finalI + 1), logger).doJob(); } });}
执行后果:
因为只有优先级最重要的工作在主线程执行,其余工作都是异步执行,所以主线程工作执行耗费57ms,所有工作执行耗费1267ms。
3.2 IO优化
3.网络申请优化。
- 启动过程中防止不必要的网络申请。对于那些在启动时非必要执行的网络申请,能够延时申请或者应用缓存。
- 对于须要进行屡次串行网络申请的接口进行优化整合,管制好申请接口的粒度。比方后盾有获取用户信息的接口、获取用户举荐信息的接口、获取用户账户信息的接口。这三个接口都是必要的接口,且存在先后关系。如果顺次进行三次申请,那么工夫基本上都花在网络传输上,尤其是在网络不稳固的状况下耗时尤为显著。但如果将这三个接口整合为获取用户的启动(初始化)信息,这样数据在网络中传输的工夫就会大大节俭,同时也能进步接口的稳定性。
4.磁盘IO优化
- 启动过程中防止不必要的磁盘IO操作。这里的磁盘IO包含:文件读写、数据库(sqlite)读写和SharePreference等。
- 对于启动过程中所必须的数据加载,抉择适合的数据结构。能够抉择反对随机读写、延时解析的数据存储构造以代替SharePreference。
- 启动过程中防止大量的序列化和反序列化。
3.3 线程优化
咱们在开发利用的过程中,都或多或少会应用到线程。当咱们创立一个线程时,须要向零碎申请资源,分配内存空间,这是一笔不小的开销,所以咱们平时开发的过程中都不会间接操作线程,而是抉择应用线程池来执行工作。
但问题就在于如果线程池设置不对的话,很容易被人滥用,引发内存溢出的问题。而且通常一个利用会有多个线程池,不同性能、不同模块乃至是不同三方库都会有本人的线程池,这样大家各用各的,就很难做到资源的协调对立,劲不往一处使。
3.3.1 线程优化剖析
1.线程池耗时剖析。
要想进行线程优化,首先咱们就须要理解线程池在应用过程中,哪些地方比拟耗时。
- 首先,从惯例上来讲,线程池在应用过程中,线程创立、线程切换和CPU调度比拟耗时。
- 其次,须要从业务的角度去剖析以后利用的线程应用状况,到底是哪些工作导致线程池大量的创立和执行耗时。这里咱们能够应用Hook的形式,去Hook线程的创立(ThreadFactory的
newThread
办法)和执行进行统计。 - 最初,咱们须要联合业务的重要性(优先级)以及其相应的线程执行开销,进行综合考量,调整线程池的执行策略。
2.线程池执行剖析。
而后再让咱们看看线程池的执行逻辑:
咱们晓得,一个线程池通常由一个外围线程池和一个阻塞队列组成。那么当咱们调用线程池去执行一个工作的时候,线程池是如何执行的呢?
外围线程池 -> 阻塞队列 -> 最大线程数(新建线程) -> RejectedExecutionHandler(回绝策略)
- 当线程池中的外围线程池线程饱和后,工作会被塞进阻塞队列中期待。
- 如果阻塞队列也满了,线程池会创立新的线程。
- 然而如果目前线程池中的线程总数曾经达到了最大线程数,这个时候会调用线程池的回绝策略(默认是间接中断,抛出异样)。
其中外围线程池的最大线程数并不是设置的越大越好,为什么这么说?因为CPU的解决能力是无限的,四核的CPU一次也只能同时执行四个工作,如果外围线程池数设置过大,那么各工作之间就会相互竞争CPU资源,加大CPU的调度耗费,这样反而会升高线程池的执行效率。
一般来说,外围线程池的最大线程数满足上面的公式:
最佳外围线程数目 = ((线程等待时间 + 线程CPU工夫)/ 线程CPU工夫 )* CPU数目
(1)线程等待时间所占比例越高,须要越多线程。
(2)线程CPU工夫所占比例越高,须要越少线程。
这里咱们思考两种最常见的状况:
- CPU密集型工作:这种工作绝大多数工夫都在进行CPU计算,线程等待时间简直为0,因而这时最佳外围线程数为 n + 1(或者为n),这里n为CPU外围数。
- IO密集型工作:这种工作,CPU通常须要期待I/O(读/写)操作,这样CPU的解决工夫和等待时间简直差不多,因而这时最佳外围线程数为 2n + 1(或者为2n),这里n为CPU外围数。
3.3.2 线程优化指标
通过上面对线程池的剖析,咱们能够晓得:
- 线程池设置过大,会强占内存,相互竞争CPU资源,减少CPU的调度耗时。
- 线程池设置过小,工作会阻塞串行,升高线程池执行效率。
因而,咱们线程优化的指标是:
(1)领有能够兼顾全局的对立的线程池。
(2)能依据机器的性能来管制数量,正当调配线程池大小。
(3)可能依据业务的优先级进行调度,优先级高的先执行。
3.3.3 线程优化具体措施
1.建设主线程池+副线程池的组合线程池,由线程池管理者对立协调治理。主线程池负责优先级较高的工作,副线程池负责优先级不高以及被主线程池回绝降级下来的工作。
这里执行的工作都须要设置优先级,工作优先级的调度通过PriorityBlockingQueue
队列实现,以下是主副线程池的设置,仅供参考:
- 主线程池:外围线程数和最大线程数:2n(n为CPU外围数),60s keepTime,PriorityBlockingQueue(128)。
- 副线程池:外围线程数和最大线程数:n(n为CPU外围数),60s keepTime,PriorityBlockingQueue(64)。
2.应用Hook的形式,收集利用内所以应用newThread
办法的中央,改为由线程池管理者对立协调治理。
3.将所有提供了设置线程池接口的第三方库,通过其凋谢的接口,设置为线程池管理者治理。没有提供设置接口的,思考替换库或者插桩的形式,替换线程池的应用。
3.4 闪屏优化
闪屏优化属于启动用户体验的优化。毕竟谁也不想应用页面一闪一闪的利用。
1.设置自定义闪屏页。
设置自定义的闪屏页能够进步咱们启动的"视觉速度"。通常会设置一个背景,而后把logo居中显示,能够应用xml文件来布局(留神,该图片不可展现动画,并且展现工夫也不可控)。这种形式能够给用户一种启动十分快的感觉,不仅解决了启动白屏的问题,并且展现了品牌logo也有助于晋升品牌认知。
(1) 应用xml自定义一张带有logo的图片。
<?xml version="1.0" encoding="utf-8"?><layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque"> <item android:drawable="?attr/xui_config_color_splash_bg"/> <item android:bottom="?attr/xui_config_app_logo_bottom"> <bitmap android:gravity="center" android:src="?attr/xui_config_splash_app_logo"/> </item> <item android:bottom="?attr/xui_config_company_logo_bottom"> <bitmap android:gravity="bottom" android:src="?attr/xui_config_splash_company_logo"/> </item></layer>
(2) 把这张图片通过设置主题的android:windowBackground
属性形式显示为启动闪屏。
<style name="XUITheme.Launch.Demo"> <item name="android:windowBackground">@drawable/xui_config_bg_splash</item> <item name="xui_config_splash_app_logo">@drawable/ic_splash_app_logo_xui</item> <item name="xui_config_splash_company_logo">@drawable/ic_splash_company_logo_xuexiang</item></style>
(3) 在manifest中将主页的主题设置为方才带启动图片的Launch
主题。
<activity android:name=".activity.MainActivity" android:theme="@style/XUITheme.Launch.Demo"></activity>
(4) 代码执行到主页面的onCreate的时候设置为程序失常的主题,这样就切回到失常主题背景了。
public class BaseActivity extends XPageActivity { @Override protected void onCreate(Bundle savedInstanceState) { setTheme(R.style.AppTheme); super.onCreate(savedInstanceState); }}
2.应用Android12提供的启动画面Splash Screen。
https://developer.android.google.cn/guide/topics/ui/splash-screen
3.如果是因为某些不可抗拒的起因(例如一些大型游戏的首次加载),导致第一次启动十分慢的话,能够在主页面进入之前,减少一个进度条加载页面。这样的益处就是要通知用户,到底须要多久能力加载结束,给用户一个明确的信息,缓解用户的期待焦虑。
4.缩小首页的跳转档次。除非某些不可抗拒的起因(比方广告作为一项重要支出起源),尽量不要设置启动页(SplashActivity),因为关上和敞开任何一个Activity都是须要耗费工夫的,每多一个Activity的跳转就意味着咱们主页面的关上工夫会被缩短。
3.5 主页面优化
主页面的启动和显示也是app启动十分重要的一部分。
3.5.1 布局优化
布局优化的外围就是:进步页面渲染的速度,避免页面适度渲染导致耗时。
- 升高视图层级。缩小冗余或者嵌套布局,避免页面适度渲染。正当应用
merge
标签和束缚布局ConstraintLayout
。 - 不必要的布局提早加载。用
ViewStub
代替在启动过程中不须要显示的UI
控件。 - 首页懒加载。首页不须要立刻显示的页面,能够应用懒加载。
- 应用自定义
View
代替简单的View
叠加。
3.5.2 页面数据预加载
一般来说,咱们喜爱在每个页面外部才开始加载和显示数据,因为这样写可能更容易让人看懂,利于当前的保护。然而如果等页面UI布局初始化结束后,咱们才去加载数据的话,势必会减少页面启动显示的工夫。
因为每个页面(Activity)的启动自身就是比拟耗时的过程,咱们能够将须要显示的数据进行预加载(即页面启动和数据加载同时进行,串行->并行),这样等页面UI布局初始化结束后,咱们就能够拿着预加载的数据间接渲染显示了,这样能够缩小数据加载的期待,从而达到放慢页面显示的目标。
这里咱们能够参考开源预加载库:PreLoader
3.6 系统调度优化
- 1.启动阶段不启动子过程,只在主过程执行Application的
onCreate
办法。因为子过程会共享CPU的资源,导致主过程CPU缓和。 - 2.启动过程中缩小零碎调用,防止与
AMS
、WMS
竞争锁。因为AMS
和WMS
在利用启动的过程中承当了很多工作,且这些办法很多都是带锁的,这时利用该当防止与它们进行通信,避免出现大量的锁期待,阻塞要害操作。 - 3.启动过程中除了
Activity
之外的组件启动要审慎。因为四大组件的启动都是通过主线程Handler进行驱动的,如果在利用启动的同时他们也启动,Handler中的Message势必会减少,从而影响利用的启动速度。 - 4.启动过程中缩小主线程Handler的应用。起因同下面一样,Activity的启动都是由主线程Handler进行驱动的,利用启动期间缩小主线程Handler的应用,能够减小对主页面启动的影响。对于那些量大且频繁的任务调度,能够应用
HandlerThread
中的Looper
创立属于子线程的handler来代替。 - 5.应用IdleHandler。利用
IdleHandler
个性,在音讯队列闲暇时,对提早工作进行分批初始化。
3.7 GC 优化
启动过程中该当缩小GC
的次数。因为GC会暂停程序的执行,从而会带来提早的代价。那么咱们该当如何防止频繁的GC呢?
- 1.防止进行大量的字符串操作,特地是序列化和反序列化。不要应用+(加号)进行字符串拼接。
- 2.防止长期对象的频繁创立,频繁创立的对象须要思考复用。
- 3.防止大量bitmap的绘制。
- 4.防止在自定义View的
onMeasure
、onLayout
和onDraw
中创建对象。
3.8 Webview启动优化
如果你的利用应用到了Webview,能够按需对Webview进行优化。
- 1.因为WebView首次创立比拟耗时,须要事后创立WebView,提前将其内核初始化。
- 2.应用WebView缓存池,用到WebView的时候都从缓存池中拿。
- 3.利用内置本地离线包,如一些字体和js脚本,即预置动态页面资源。
3.9 利用瘦身
对利用瘦身,能够最间接地放慢资源加载的速度,从而进步利用启动的效率。
- 1.删除没有援用的资源。咱们能够应用Inspect Code或者开启资源压缩,主动删除无用的资源。
- 2.能用xml写Drawable的,就不要应用UI切图。
- 3.重用资源。同一图像的着色不同,咱们能够用android:tint和tintMode属性调整应用。
- 4.压缩png和jpeg文件。这里举荐一个十分好用的图片压缩插件:img-optimizer
- 5.应用webp文件格式。Android Studio能够间接将现有的bmp,jpg,png或动态gif图像转换为webp格局。
- 6.应用矢量图形,尤其是那些与分辨率无关,且可伸缩的小图标尽可能应用矢量图形。
- 7.开启代码混同。应用proGuard代码混同器工具,包含压缩、优化、混同等性能。
- 8.对于一些独立也非必须的功能模块,采纳插件化,按需加载。
3.10 资源重排
利用Linux的IO读取策略,PageCache和ReadAhead机制,依照读取程序重新排列,缩小磁盘IO次数。具体可参见《支付宝 App 构建优化解析:通过安装包重排布优化 Android 端启动性能》 这篇文章。这种技术门槛较高,个别利用都不会用到。
3.11 类重排
类重排的实现通过ReDex的Interdex调整类在Dex中的排列程序。调整Dex中类的程序,把启动时须要加载的类按程序放到主dex里。具体实现能够参考《Redex初探与Interdex:Andorid冷启动优化》 这篇文章。
4. 如何进行优化
下面讲了那么多利用启动优化的策略和措施,可能有些人就会问了:那么具体到咱们每个不同的我的项目上,咱们应该如何进行优化呢?
以下是我集体的优化步骤,仅供参考:
- 1.明确优化的内容和指标。首先,做任何优化肯定是须要带着问题(目标)去优化的。任何不带目标进行的优化都是耍流氓。
- 2.剖析现状、确认问题。当咱们目前须要优化的内容后,接下来就是须要进行大量的埋点统计、比拟与剖析,确认到底是因为什么起因导致的利用启动过慢,找到须要优化的部位。
- 3.进行针对性的优化。找到导致利用启动过慢的问题之后,就是依照本篇讲述的优化策略和措施,进行针对性的优化。
- 4.对优化后果进行总结并进行继续跟进。对优化前后的数据进行统计和比拟,总结优化的教训并在组内进行分享,并在后续的版本中进行继续跟进。有条件的能够联合CI,减少线上的启动性能监控。
最初
讲了这么多,还是心愿大家在平时开发的过程中,多器重一些利用启动优化的相干技巧,这样等他人让你优化利用启动的时候,也就不会那么不知所措了。
我是xuexiangjys,一枚酷爱学习,喜好编程,勤于思考,致力于Android架构钻研以及开源我的项目教训分享的技术up主。获取更多资讯,欢送微信搜寻公众号:【我的Android开源之旅】
本文由mdnice多平台公布