Android-UI组件合集面试资料分享

开头

在一般的互联网公司的技术人员的面试中,大概会经历3到4轮的面试,差不多2-3轮的技术面,还有1轮的HR面试,有人面试题是有关“目标”,有的关于“方法”,有的关于“算法”,有的关于“基础”。在小编看来,面试是找工作的第一道难关,原本以为面试都会问很难的问题,如果是更好的公司,可能问的问题越难。

背景

开发者文档中提到,Android应用有三种启动状态,每种状态都会影响应用向用户显示所需的时间:冷启动、温启动或热启动。三种启动状态中,冷启动耗时最久,系统和App有较多初始化的工作。如果启动时间过长,可能会导致用户在应用商店打低分,甚至完全弃用app,所以冷启动速度是各个app非常重要的性能指标之一。

在冷启动速度优化的工作中,打点是非常重要的一环,统计点位该如何选,以及为什么要这么选,有很多细节值得探究,本文主要深入探究Android端app层如何选择进程创建的起点。

三个时机简述

本文中涉及的3个App层进程创建时间的起点:Application ,Process.getStartElapsedRealTime,/proc/self/stats starttime。

简单介绍下3个进程创建时间起点:

  • Application :Application构造方法;
  • Process.getStartElapsedRealTime:Framework中记录的进程创建的起点,此接口有版本限制,Android N以下版本无法使用;
  • /proc/self/stats starttime:内核中记录的进程创建的起点。

3个进程创建时间起点时序如下:/proc/self/stats starttime 早于 Process.getStartElapsedRealTime 早于 Application

Android-UI组件合集面试资料分享_第1张图片

这三个时机哪个更好?哪个能指导优化工作?哪个更接近用户点击桌面创建进程的起始点?带着几个问题,继续往下看。

深入分析

详细看下三个时机:

  • Application 时机

Applciation的构造方法,Android Java代码可以最先埋点的时机,Android开发童鞋对此时机都会比较熟悉,不过多赘述。

  • Process.getStartElapsedRealTime时机

    时序总览图:

Android-UI组件合集面试资料分享_第2张图片

Process.getStartElapsedRealTime的赋值接口为handleBindApplication接口,赋值时机为App进程进入Java世界后,进程attach到ActivityManagerService,再通过binder call返回到App进程时。原理细节可继续阅读源码解析。

源码解析:

Android 8.1.0的源码中一段说明(Process.java):
487    /**
488     * Return the {@link SystemClock#elapsedRealtime()} at which this process was started.
489     */
490    public static final long getStartElapsedRealtime() {
491        return sStartElapsedRealtime;
492    }

从源码的说明中可知,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构造方法中打断点情况如下:
Android-UI组件合集面试资料分享_第3张图片

/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()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5rFmtSoQ-1623416459815)(https://upload-images.jianshu.io/upload_images/22459598-0ad6f754e0dca6e3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

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

Android-UI组件合集面试资料分享_第4张图片

Android-UI组件合集面试资料分享_第5张图片

Android-UI组件合集面试资料分享_第6张图片

Android-UI组件合集面试资料分享_第7张图片

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以下机型无法获取,则会有大量的低端机器的启动性能不在统计范围内。

  • 可能有的童鞋还会有一个疑问,为什么说/proc/self/stats starttime更接近用户的实际启动体验,而不是用户的全部启动体验呢?熟悉应用程序启动过程的同学就会比较了解这个问题,Android应用程序启动是从用户点击桌面图标开始,点击图标的第一响应是在Launcher进程,通过ActivityManagerService将创建进程信息传给zygote,zygote再执行fork,中间经历了两次跨进程通信,一次是Launcher进程通过Binder调用进入system_server进程,一次是system_server进程通过socket将创建进程信息传给zygote,zygote从睡梦中醒来,开始创建进程,细节不赘述了,感兴趣的童鞋可以搜下相关资料,在网上有很多教程。

尾声

改变人生,没有什么捷径可言,这条路需要自己亲自去走一走,只有深入思考,不断反思总结,保持学习的热情,一步一步构建自己完整的知识体系,才是最终的制胜之道,也是程序员应该承担的使命。

以上进阶Android高级工程师系统学习资料可以免费分享给大家,需要完整版的朋友,点这里可以看到全部内容。

如果需要PDF版本可以在群文件夹里,自行领取!

  • 或者在群文件夹中里,自行下载直达领取链接:【https://links.jianshu.com/go?to=https%3A%2F%2Fjq.qq.com%2F%3F_wv%3D1027%26k%3DBRZhpPkt】

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

总结

其实要轻松掌握很简单,要点就两个:

  1. 找到一套好的视频资料,紧跟大牛梳理好的知识框架进行学习。
  2. 多练。 (视频优势是互动感强,容易集中注意力)

你不需要是天才,也不需要具备强悍的天赋,只要做到这两点,短期内成功的概率是非常高的。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。下面资料部分截图是我花费几个月时间整理的,诚意满满:特别适合有3-5年开发经验的Android程序员们学习。

  • 自行下载直达领取链接:【点击我即可获得!】
  • 以上进阶BATJ大厂学习资料可以免费分享给大家,需要完整版的朋友,【点这里可以看到全部内容】。

验的Android程序员们学习。**

  • 自行下载直达领取链接:【点击我即可获得!】
  • 以上进阶BATJ大厂学习资料可以免费分享给大家,需要完整版的朋友,【点这里可以看到全部内容】。

[外链图片转存中…(img-mLoEycoO-1623416459821)]

你可能感兴趣的:(程序员,android,移动开发,经验分享)