当前位置: 首页 > news >正文

Android 启动优化

为什么做启动优化

APP启动值得是用户从桌面点击APP icon 到APP 页面第一帧数据渲染出来的时间,如果启动时间过长会让用户满意度下降,甚至放弃使用,因此我们要保证功能前提下,尽量降低启动时间

首先复习一下App启动

Android App启动过程

Android ContentProvider启动流程

  • 首先是点击App图标,此时是运行在Launcher进程,通过ActivityManagerServiceBinder IPC的形式向system_server进程发起startActivity的请求
  • system_server进程接收到请求后,通过Process.start方法向zygote进程发送创建进程的请求
  • zygote进程fork出新的子进程,即App进程
  • 然后进入ActivityThread.main方法中,这时运行在App进程中,通过ActivityManagerServiceBinder IPC的形式向system_server进程发起attachApplication请求
  • system_server接收到请求后,进行一些列准备工作后,再通过Binder IPC向App进程发送scheduleLaunchActivity请求
  • App进程binder线程(ApplicationThread)收到请求后,通过Handler向主线程发送LAUNCH_ACTIVITY消息
  • 主线程收到Message后,通过反射机制创建目标Activity,并回调ActivityonCreate

而我们的Appcation的启动是在第四步的attachApplication中请求的开始的,下面我们就具体看源码分析

ActivityManagerService.java

 public final void attachApplication(IApplicationThread thread, long startSeq) {synchronized (this) {int callingPid = Binder.getCallingPid();final int callingUid = Binder.getCallingUid();final long origId = Binder.clearCallingIdentity();attachApplicationLocked(thread, callingPid, callingUid, startSeq);Binder.restoreCallingIdentity(origId);}}
private final boolean attachApplicationLocked(IApplicationThread thread,int pid) {....thread.bindApplication(processName, appInfo, providers,app.instr.mClass,profilerInfo, app.instr.mArguments,app.instr.mWatcher,app.instr.mUiAutomationConnection, testMode,mBinderTransactionTrackingEnabled, enableTrackAllocation,isRestrictedBackupMode || !normalMode, app.persistent,new Configuration(getGlobalConfiguration()), app.compat,getCommonServicesLocked(app.isolated),mCoreSettingsObserver.getCoreSettingsLocked(),buildSerial);
}

这里调用了threadbindApplication方法,thread的类型是IApplicationThread,是一个binder用于跨进程通信,实现类是ActivityThread的内部类ApplicationThread

App 的 application 创建是在 ActivityThread 的 handleBindApplication 方法完成的。

    private void handleBindApplication(AppBindData data) {...// If the app is Honeycomb MR1 or earlier, switch its AsyncTask// implementation to use the pool executor.  Normally, we use the// serialized executor as the default. This has to happen in the// main thread so the main looper is set right.if (data.appInfo.targetSdkVersion <= android.os.Build.VERSION_CODES.HONEYCOMB_MR1) {AsyncTask.setDefaultExecutor(AsyncTask.THREAD_POOL_EXECUTOR);}...final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);...app = data.info.makeApplication(data.restrictedBackupMode, null);...mInitialApplication = app;...try {mInstrumentation.callApplicationOnCreate(app);...}

handleBindApplication 通过 LoadedApk 的 makeApplication 构造 application。

LoadedApk 的 makeApplication 方法如下:

    public Application makeApplication(boolean forceDefaultAppClass,Instrumentation instrumentation) {...ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);app = mActivityThread.mInstrumentation.newApplication(cl, appClass, appContext);appContext.setOuterContext(app);...return app;}

makeApplication 通过 ContextImpl.createAppContext 构造了 contextImpl,然后用 newApplication 构造了 application。

Instrumentation 的 newApplication 方法如下:

    public Application newApplication(ClassLoader cl, String className, Context context)throws InstantiationException, IllegalAccessException, ClassNotFoundException {Application app = getFactory(context.getPackageName()).instantiateApplication(cl, className);app.attach(context);return app;}

可以看出 newApplication 先构造了 Application,然后再 attach context,这一点和 Activity 的构造类似,都是先创建,再 attach 一些内容。

Application 的 attach 方法如下:

    /* package */ final void attach(Context context) {attachBaseContext(context);mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;}

到这里就调用了attachBaseContext

看了上面分析和文章,你就会发现,这个流程系统启动过程我们是无法干预的,但是当启动到了自己的App进程,就可以知己处理了

  • Appcation 构造方法
  • Appcation attachBaseContext
  • ContentProvider启动
  • Appcation onCreate
  • Activity onCreate
  • Activity onStart
  • Activity onResume
  • View onDraw
  • Activity#onWindowFocusChanged()

这几个流程就是我们可以控制的流程,我们的优化也是在这几个流程中进行优化

启动时间检测

查看Logcat关键字 Displayed

在Android Studio Logcat中过滤关键字“Displayed”,可以看到对应的冷启动耗时日志。

ActivityTaskManager: Displayed com.example.wanandroid/.MainActivity: +331ms

这种方式最简单,适用于收集 App 与竞品 App 启动耗时对比分析。

adb shell 命令查看启动耗时

adb shell am start -W [packageName]/[启动activity的全路径]

比如

adb shell am start -W com.example.wanandroid/com.example.wanandroid.MainActivity

然后会有结果

Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.wanandroid/.MainActivity }
Status: ok
LaunchState: WARM
Activity: com.example.wanandroid/.MainActivity
TotalTime: 348
WaitTime: 351
Complete
  • TotalTime 表示所有activity 的启动耗时
  • WaitTime 表示aws 启动activity的耗时

TotalTime 就是应用启动耗时 他包括进程启动+Application启动+Activity初始化到ui显示时间
这种适合线下使用,对比竞品的启动速度

前面提到的俩种方式,统计的是App启动到Activity首次调用onWindowFocusChanged的时间,如果我们想要统计,App启动到网络数据请求之后的总耗时,可以在终点调用, activity.reportFullyDrawn(),通知当前已经绘制完成,然后就可以在Locat中看到

Displayed com.example.wanandroid/com.example.wanandroid.MainActivity: +3s171ms
Fully drawn com.example.wanandroid/com.example.wanandroid.MainActivity: +4s459ms

自己打点计时

上面我们已经分析APP的启动流程

  • Appcation 构造方法
  • Appcation attachBaseContext
  • ContentProvider启动
  • Appcation onCreate
  • Activity onCreate
  • Activity onStart
  • Activity onResume
  • View onDraw
  • Activity#onWindowFocusChanged()

我们自己打点可以在Appcation attachBaseContext 作为起始点,Activity#onWindowFocusChanged() 作为终点来计算真正的启动时间

终点的选择其实有俩种第一种就是Activity#onWindowFocusChanged() 这种的话可能不是首帧,可能是2-3帧,统计时间会多几帧的耗时,但是相对来说稳定

如果首帧的渲染需要通过网络请求之后才能渲染数据,那么这个就不是很准确,因为Activity#onWindowFocusChanged() 统计的是默认UI的显示(就是写死的ui)

这种情况可以选择列表的第一个ItemView的perDrawCallback() 回调来计算真正的时间,当列表的第一个Item 显示数据,表示已经网络加载完成

// itemView添加预绘制回调监听
itemView.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {override fun onPreDraw(): Boolean {return false}
})

获取启动各个阶段的耗时

上面我们计算了App启动的整体耗时,这个只能得出总耗时的结论,我们还需要知道启动各个阶段的耗时,这样我们才能确定到底是哪里的代码导致的,获取各个阶段的耗时,一般有俩种方法

  • 手动埋点
  • 编译时AOP

手动埋点

手动埋点比较简单,在需要的地方加上统计代码即可

编译时AOP

编译时AOP表示在编译期间要对耗时的函数进行插桩,在他计时前后进行耗时统计,这个后序单独出一篇博客讲解

启动优化工具

除了获取启动时间,线下测试如果想要更进一步的获取启动耗时点,可以使用工具进行进一步分析,比如TraceView,CPU Profiler SysTrace Perfetto 这些之后也会单独开一篇文章

启动优化方案

视觉优化启动速度

在冷启动的时候

  • 启动后立即显示应用程序空白的启动窗口。
  • 创建应用程序进程。

也就是说点击app后会先显示一个空白的window,如果启动时间过长,这样启动就会显示白框很久,所以体验会很不好

这种的解决办法就是,设置闪屏主题

    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"><item name="android:windowBackground">@drawable/lunch</item>  //闪屏页图片<item name="android:windowFullscreen">true</item><item name="android:windowDrawsSystemBarBackgrounds">false</item><!--显示虚拟按键,并腾出空间--></style>

把这个style设置给闪屏activity,其他的不用换,这样就可以直接显示闪屏图片,解决白屏问题,但是这个是治标不治本

减少ContenProvider 初始化耗时

由于ContenProvider 有自动初始化的特性,在App启动时会自动调用onCreate() 并且是在主线程进行,很多框架都喜欢在ContenProvider#onCreate()进行初始化SDK,这样的初始化代码多了以后,很容易出现耗时比较长的代码(比如数据库查询),直接影响启动速度

一种比较好的处理方式,提供一个ContenProvider的封装类,和一个异步的onCreate方法,然后再编译时将继承ContenProvider的类,改为继承这个基类,同时把onCreate 改为 onCrateAsycn

这样就是实现了ContenProvider#onCreate()的异步化,减少对启动的影响

或者使用jetpack中的App Startup 也能起到优化作用,通过App StartupInitializer,将多个ContenProvider完成的初始化工作,合并到同一个ContenProvider,减少创建ContenProvider的成本

Application 阶段的优化

在应用启动的时候,可能有很多组件需要进行初始化,这样的初始化多了以后就会拖累启动速度,对于这种我们有俩种决解决思路

  • 异步化
  • 延迟加载

异步化

异步化理解起来很简单,就是把任务放到子线程,但是各个组件的初始化可能有依赖关系,如果只是简单的子线程恐怕达不到目的,所以我们需要一种框架他需要支持一下几个特点

  • 根据任务类型决定运行任务的线程
  • 根据依赖关系,自动将前一个依赖任务执行
  • 根据任务优先级顺序执行任务

比如阿里巴巴的开源框架 alpha 通过配置,生成有向无环图,俩保证依赖顺序,通过配置将任务分配到合理的线程

延迟优化

对于一些启动并优先级不高的的组件我们可以选择延迟优化,延迟优化,我们可以选择利用IdleHandler,只有在主线程空闲时才执行任务

Activity 阶段优化

这个阶段主要分为俩步

  • 布局优化
  • 本地数据缓存

布局优化

布局越复杂,加载布局的时间就越长,所以注意下面几点

  • 删除无用布局(再需求迭代中,有些布局不在使用)
  • 使用Viewstub 对布局进行懒加载
  • 降低布局层级
  • 使用megra 减少布局层级

也可以对布局进行预加载

加载XML 有以下三个流程

  • 将XML 文件加载到内存中, XmlResourceParser 的 IO 过程
  • 根据不同的name 反射View对象
  • 最终形成View树

这块我们可以直接用代码写布局代替xml,或者使用在 androidx 中已经有提供了 AsyncLayoutInflater 用于进行 xml 的异步加载

本地数据缓存

正常情况下,App启动都是需要请求网络,然后再进行数据渲染,我们可以把上一次的数据缓存下来,下一次启动的时候,直接加载缓存数据,来达到降低启动时间的目的

其他优化方式

绑定大核提升启动速度

CPU根据频率,cache(高速缓存)大小等,区分为大核和小核,一般大核执行频率更高,我们可以代码查看CPU的最高频率

    Process proc = Runtime.getRuntime().exec("cat /sys/devices/system/cpu/cpu5/cpufreq/cpuinfo_max_freq");proc.waitFor();InputStream inputStream = proc.getInputStream();reader = new BufferedReader(new InputStreamReader(inputStream));resultBuilder = new StringBuilder();String line = "";while (null != (line = reader.readLine())) {resultBuilder.append(line);}String result = resultBuilder.toString();

然后绑定大核

  • 通过CPU_ZERO初始化一个cpu_set_t。
  • 通过CPU_SET设置进程运行在哪个CPU上,可以调用多次。
  • 执行sched_setaffinity设置亲和度。
void set_affinity() {cpu_set_t set;CPU_ZERO(&set);CPU_SET(3, &set);CPU_CLR(2, &set);int ret = sched_se taffinity(0, sizeof(cpu_set_t), &set);LOG("set_affinity ret: %d", ret);struct timeval time{};time.tv_sec = 3;select(0, nullptr, nullptr, nullptr, &time);int cpu = sched_getcpu();LOG("after sleep, run in cpu%d", cpu);get_affinity();
}//测试代码入口函数
void affinity_test() {LOG(" ");LOG(" affinity_test >> max cpu_set size: %d", CPU_SETSIZE);int cpu = sched_getcpu();LOG("run in cpu%d", cpu);set_affinity();
}

区分机型 低 中 高端机 优化分类

  • 任务延时时间,在低端机上适当延迟久一些
  • 任务异步,在低端机要考虑线程数,低端机减少是线程数
  • 通过ClassLoader记录启动的类及耗时,然后再启动时异步加载需要的类,从而减少加载耗时

参考

Android 性能优化与实践

相关文章:

  • Python os 模块简介及基础使用示例
  • 在PyTorch中,对于一个张量,如何快速为多个元素赋值相同的值
  • 【笔记】解决ImportError: cannot import name ‘Iterable‘ from ‘collections‘
  • 【会议推荐】2025年模式识别与大数据国际会议(PRBD 2025)
  • 华为认证中HCIA/HCIP/HCIE是什么等级?怎么考试?
  • #跟着Lucky学鸿蒙# HarmonyOS NEXT 工程介绍
  • ES 在大查询场景下导致 GC 频繁,如何定位和解决?
  • 用 Python 打造你的专属虚拟试衣间!——AI+AR 如何改变时尚体验
  • 模型评价指标介绍
  • emqx、MongoDB或者java程序,出现 Too many open files 问题
  • Flink系列文章列表
  • 自动化测试入门:解锁高效软件测试的密码
  • DAY 38 Dataset和Dataloader类
  • 判断元素是否获取焦点
  • 英码科技携带 “无感知AI数字课堂”解决方案,亮相第22届广东教育装备展
  • 哈希算法:原理、应用、安全演进与推荐
  • 计算机网络学习20250527
  • 科技趋势分析系统(BBC)技术全解
  • 【数据结构】树形结构--二叉树
  • 【数据结构】 时间复杂度
  • 微信小商城怎么开通/seo站长论坛
  • 网站后台数据处理编辑主要是做什么的啊/中国刚刚发生8件大事
  • 网站建设 济南/网站权重怎么看
  • 门户网站快速制作/凡科建站官网入口
  • 长沙3合1网站建设价格/网店推广分为哪几种类型
  • 广东省建设厅网站6/广告招商