熬了3天3夜——深入探究Android应用启动起点

从源码的说明中可知,Process.getStartElapsedRealTime代表程序创建开始的时间,
SystemClock#elapsedRealtime表示距离boot的真实时间,看下其赋值时机(ActivityThread.java):
5429 private void handleBindApplication(AppBindData data) {…
5436 // Note when this process has started.
5437 Process.setStartTimes(SystemClock.elapsedRealtime(), SystemClock.uptimeMillis());

handleBindApplication是在ActivityThread主线程H的消息处理中被调用的,
H作为ActivityThread的内部类,是主线程处理消息的Handler。
234 final H mH = new H();

这个消息是谁发的,什么时候发的呢?了解Android App的入口函数及创建过程的同学,可能不难解答这个问题。
App的创建,Java层调用的入口为ActivityThread main方法,看下:
6459 public static void main(String[] args) {…
6478 Looper.prepareMainLooper();
6479
6480 ActivityThread thread = new ActivityThread();
6481 thread.attach(false);…
6494 Looper.loop();

从代码中看,main方法中主要是准备主线程消息Looper,执行ActivityThread attach方法,然后主线程开始消息循环。
看下ActivityThread attach:
6315 private void attach(boolean system) {
6318 if (!system) {
6328 final IActivityManager mgr = ActivityManager.getService();
6329 try {
6330 mgr.attachApplication(mAppThread);
6331 } catch (RemoteException ex) {
6332 throw ex.rethrowFromSystemServer();
6333 }

从代码可知,此处有binder调用,调用AMS的attachApplication,此调用是在system_server进程,执行如下操作。
看下ActivityManagerService处理过程:
7215 public final void attachApplication(IApplicationThread thread) {
7216 synchronized (this) {
7219 attachApplicationLocked(thread, callingPid);
7221 }
7222 }
6911 private final boolean attachApplicationLocked(IApplicationThread thread,
6912 int pid) {…
7102 thread.bindApplication(processName, appInfo, providers,
7103 app.instr.mClass,
7104 profilerInfo, app.instr.mArguments,
7105 app.instr.mWatcher,
7106 app.instr.mUiAutomationConnection, testMode,
7107 mBinderTransactionTrackingEnabled, enableTrackAllocation,
7108 isRestrictedBackupMode || !normalMode, app.persistent,
7109 new Configuration(getGlobalConfiguration()), app.compat,
7110 getCommonServicesLocked(app.isolated),
7111 mCoreSettingsObserver.getCoreSettingsLocked(),
7112 buildSerial);

比较关键的调用:thread.bindApplication, thread是Binder对象,这个地方又有binder调用,看看执行者:
690 private class ApplicationThread extends IApplicationThread.Stub {
899 public final void bindApplication(String processName, ApplicationInfo appInfo,
900 List providers, ComponentName instrumentationName,
901 ProfilerInfo profilerInfo, Bundle instrumentationArgs,
902 IInstrumentationWatcher instrumentationWatcher,
903 IUiAutomationConnection instrumentationUiConnection, int debugMode,
904 boolean enableBinderTracking, boolean trackAllocation,
905 boolean isRestrictedBackupMode, boolean persistent, Configuration config,
906 CompatibilityInfo compatInfo, Map services, Bundle coreSettings,
907 String buildSerial)
A
pplicationThread执行sendMessage(H.BIND_APPLICATION, data);

将消息发送出去,此部分的执行为App进程的binder线程池里,是如何切换至主线程执行的呢?
2605 private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
2609 Message msg = Message.obtain();…
2617 mH.sendMessage(msg);
2618 }

通过mH,将消息发送到主线程的Looper,主线程执行,
1462 private class H extends Handler {
1473 public static final int BIND_APPLICATION = 110;
1580 public void handleMessage(Message msg) {
1653 case BIND_APPLICATION:
1656 handleBindApplication(data);
1658 break;

handleBindApplication就是Process.getStartElapsedRealTime获取对进程创建的起点,后续逻辑就是Application的初始化的工作,由此可见Process.getStartElapsedRealTime时机是比Application时机早,在Application构造方法中打断点情况如下:

/proc/self/stats starttime时机

/proc/self/stats starttime时机是kernel层记录的进程创建起点,为3个时机中最早的。详细看下:

proc/pid/stat用于获取某一个进程的统计信息,内容形式如下:

在proc/pid/stat统计信息中,starttime为第22个元素。starttime的值什么含义,以及是如何计算出来的呢?看下fs/proc/array.c的do_task_stat()

从内核代码中可知:start_time取值为task的real_start_time,先看下nesc_to_clock_t方法:

div_u64_rem方法为无符号除法操作:除数是无符号64bit,被除数是无符号32,remainder为余数。

从计算过程来看,是把real_start_time除以1000000000/100=10000000,real_start_time单位是什么呢?看下数据结构task_struct定义:

struct timespec start_time;
struct timespec real_start_time;

task_struct中有两个时间:start_time 和 real_start_time,其中后者包含睡眠时间,两个时间单位均为ns,/proc/self/stats starttime取的值为real_start_time:

struct timespec
{
__time_t tv_sec; /* Seconds. /
long tv_nsec; /
Nanoseconds. */
};

由此可见,real_start_time单位为ns,如果将real_start_time除以1000000000/100=10000000,换算完单位为10ms,比如/proc/self/stats starttime读取到的值为100,则需换算为100*10ms=1000ms。而我们启动速度日常大概率会以ms为计算精度,/proc/self/stats starttime会损失一定的精度,内核为何会做此种处理呢?

在内核的时间统计方式中,有个单位为jiffies,jiffies是内核中的一个全局变量,用来记录自系统启动以来产生的节拍数。简单描述就是1s内,内核发起的时钟中断次数,kernel中就使用这个来对程序的运行时间进行统计。而/proc/self/stats starttime统计单位正是jiffies,代表应用程序冷启动后经过了多少个内核时钟。

那我们该如何科学的统计以及换算/proc/self/stats starttime的值呢?Linux 系统上Man proc有下面一段解释:

(22) starttime %llu

The time the process started after system boot. In kernels before Linux 2.6, this value was expressed in jiffies. Since Linux 2.6, the value is expressed in clock ticks (divide by sysconf(_SC_CLK_TCK)).

The format for this field was %lu before Linux 2.6.

在内核态的常量USER_HZ我们无法获取,但可以通过在用户态通过sysconf(_SC_CLK_TCK)获取到其值。

计算公式如下:

/proc/self/stats starttime * 1000 / sysconf(_SC_CLK_TCK),单位ms

可能有些同学会说,sysconf(_SC_CLK_TCK)的值是100,直接用/proc/self/stats starttime * 10即可,但需考虑内核的升级或内核定制场景,使用sysconf(_SC_CLK_TCK)获取并参与计算为最稳妥的方式。

再一个问题,/proc/self/stats starttime 是来自task_struct real_start_time,这个时间初始化是在什么时候呢?答案就是task_struct数据结构被创建的时候,也就是进程被创建的时候,即 zygote fork时机,fork系统调用会把子进程的数据结构task_struct、线程栈等数据结构初始化,感兴趣的同学可以去看内核的fork源码。

总结

通过上述的详细分析,已经对三个时机有较为详细的了解。在实际App工程中,建议结合使用Application 时机和/proc/self/stats starttime时机作为应用程序启动的起点。

  • Application 时机是Android Java代码可以最先埋点的地方,通过此起点,再结合冷启动的结束点位,可明确知晓工程代码的详细耗时,对于指导日常优化工作有较大意义;
  • /proc/self/stats starttime时机为三个时机中最早的,其中有工程代码不可控的耗时,涉及到进程数据结构、线程栈等初始化工作,但是此时机会更接近用户的实际感受,可以最大程度用来衡量用户启动体验;
  • Process.getStartElapsedRealTime由于有版本的限制,在Android N以下版本无法获取,无法兼顾大盘所有的用户机器,此值的指导价值就没那么大,优化工作中,重中之重是优化中低端机器的性能体验,如果Android N以下机型无法获取,则会有大量的低端机器的启动性能不在统计范围内。

最后

为了方便有学习需要的朋友,我把资料都整理成了视频教程(实际上比预期多花了不少精力)

当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

  • 无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,这四个字就是我的建议!!
  • 我希望每一个努力生活的IT工程师,都会得到自己想要的,因为我们很辛苦,我们应得的。

当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,没有人能随随便便成功。

加油,共勉。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
开同龄人。

无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,没有人能随随便便成功。

加油,共勉。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 20
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值