Android Application启动流程

Android Application与其他移动平台有两个重大不同点:

  • 每个Android App都在一个独立空间里, 意味着其运行在一个单独的进程中, 拥有自己的VM, 被系统分配一个唯一的user ID.
  • Android App由很多不同组件组成, 这些组件还可以启动其他App的组件. 因此, Android App并没有一个类似程序入口的main()方法.

Android Application组件包括:

  • Activities: 前台界面, 直接面向User, 提供UI和操作.
  • Services: 后台任务.
  • Broadcast Receivers: 广播接收者.
  • Contexnt Providers: 数据提供者.

Android进程与Linux进程一样. 默认情况下, 每个apk运行在自己的Linux进程中. 另外, 默认一个进程里面只有一个线程—主线程. 这个主线程中有一个Looper实例, 通过调用Looper.loop()从Message队列里面取出Message来做相应的处理.

启动App流程

用户点击Home上的一个App图标, 启动一个应用时:

Click事件会调用startActivity(Intent), 会通过Binder IPC机制, 最终调用到ActivityManagerService. 该Service会执行如下操作:

  • 第一步通过PackageManager的resolveIntent()收集这个intent对象的指向信息.
  • 指向信息被存储在一个intent对象中.
  • 下面重要的一步是通过grantUriPermissionLocked()方法来验证用户是否有足够的权限去调用该intent对象指向的Activity.
  • 如果有权限, ActivityManagerService会检查并在新的task中启动目标activity.
  • 现在, 是时候检查这个进程的ProcessRecord是否存在了.

如果ProcessRecord是null, ActivityManagerService会创建新的进程来实例化目标activity.

创建进程

ActivityManagerService调用startProcessLocked()方法来创建新的进程, 该方法会通过前面讲到的socket通道传递参数给Zygote进程. Zygote孵化自身, 并调用ZygoteInit.main()方法来实例化ActivityThread对象并最终返回新进程的pid.

ActivityThread随后依次调用Looper.prepareLoop()和Looper.loop()来开启消息循环.

流程图如下:

绑定Application

接下来要做的就是将进程和指定的Application绑定起来. 这个是通过上节的ActivityThread对象中调用bindApplication()方法完成的. 该方法发送一个BIND_APPLICATION的消息到消息队列中, 最终通过handleBindApplication()方法处理该消息. 然后调用makeApplication()方法来加载App的classes到内存中.

流程如下:

启动Activity

经过前两个步骤之后, 系统已经拥有了该application的进程. 后面的调用顺序就是普通的从一个已经存在的进程中启动一个新进程的activity了.

实际调用方法是realStartActivity(), 它会调用application线程对象中的sheduleLaunchActivity()发送一个LAUNCH_ACTIVITY消息到消息队列中, 通过 handleLaunchActivity()来处理该消息.

假设点击的是一个视频浏览的App, 其流程如下:

影响启动速度的原因

高耗时任务
数据库初始化、某些第三方框架初始化、大文件读取、MultiDex加载等,导致CPU阻塞

复杂的View层级
使用的嵌套Layout过多,层级加深,导致View在渲染过程中,递归加深,占用CPU资源,影响Measure、Layout等方法的速度

类过于复杂
Java对象的创建也是需要一定时间的,如果一个类中结构特别复杂,new一个对象将消耗较高的资源,特别是一些单例的初始化,需要特别注意其中的结构

主题及Activity配置
有一些App是带有Splash页的,有的则直接进入主界面,由于主题切换,可能会导致白屏,或者点了Icon,过一会儿才出现主界面

冷启动的优化

减少在Application和第一个Activity的onCreate()方法的工作量;
不要让Application参与业务的操作;
不要在Application进行耗时操作;
不要以静态变量的方式在Application中保存数据;
减少布局的复杂性和深度;

冷启动秒开方式:
1、将背景图设置成我们APP的Logo图,作为APP启动的引导,现在市面上大部分的APP也是这么做的。

1
2
3
<style name="AppWelcome" parent="AppTheme">
<item name="android:windowBackground">@mipmap/bg_welcome_start</item>
</style>

2、将背景颜色设置为透明色,这样当用户点击桌面APP图片的时候,并不会”立即”进入APP,而且在桌面上停留一会,其实这时候APP已经是启动的了,只是我们心机的把Theme里的windowBackground的颜色设置成透明的,强行把锅甩给了手机应用厂商(手机反应太慢了啦,哈哈)。

1
<style name="Appwelcome" parent="android:Theme.Translucent.NoTitleBar.Fullscreen"/>

Dex懒加载

在APP功能日益复杂的今天,MultiDex几乎是已经无法避免了,为了启动速度的优化,可以将启动时必需的方法,放在主Dex中(即classes.dex),方法是在Gradle脚本中配置multiDexKeepFile或者multiDexKeepProguard属性(代码如下),待App启动完成后,再使用MultiDex.install来加载其他的Dex文件。这种方法风险比较高,而且实现成本比较大,如果启动依赖的库比较多,还是无法实现

1
2
3
4
5
6
7
8
android {
buildTypes {
release {
multiDexKeepFile file('multidex-config.txt') // multiDexKeepFile规则
multiDexKeepProguard file('multidex-config.pro') // 类似ProGuard的规则
}
}
}

配置文件示例:

1
2
3
4
5
6
7
8
9
10
11
# 常规的multiDexKeepFile规则

com/example/MyClass.class
com/example/MyOtherClass.class

# 类似ProGuard规则

-keep class com.example.MyClass
-keep class com.example.MyClassToo

-keep class com.example.** { *; } // All classes in the com.example package

多线程的思考

在App启动时,为了加快启动速度,通常会使用多线程手段来并行执行任务,充分发挥多核CPU的优势,提高运算效率。此方法固然能够对启动速度的优化,起到一定作用,但实际开发中,有以下几点值得深思:

并发的线程数,多少合适?(效率高但不至于阻塞)

频繁切换线程,是否带来负面影响?(频繁地从主线程扔进辅助线程操作再将结果抛回来会不会比直接执行更慢)

何时并行?何时串行?(有的任务能只能串,有的任务可以并行)

这个时候,拿Android经典的AsyncTask类来说事,再合适不过了!

1
2
3
4
5
6
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// We want at least 2 threads and at most 4 threads in the core pool,
// preferring to have 1 less than the CPU count to avoid saturating
// the CPU with background work
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;

上面的代码是AsyncTask确定线程池数量的部分,其中,核心执行池保证最少2个线程,最多不超过CPU可用核数-1,最大线程池数量为CPU核数的2倍+1

这样配置线程池的目的很简单:防止并发过大,导致CPU阻塞,影响效率

参考资料

[译]Android Application启动流程分析