写点什么

【收藏】2021 年 Android 跳槽大厂必备宝典 (1),android 教程零基础入门

用户头像
Android架构
关注
发布于: 刚刚
  • 3、线上监控及线下监测工具的建设


我做卡顿优化也是经历了一些阶段,最初我们的项目当中的一些模块出现了卡顿之后,我是通过系统工具进行了定位,我使用了 Systrace,然后看了卡顿周期内的 CPU 状况,同时结合代码,对这个模块进行了重构,将部分代码进行了异步和延迟,在项目初期就是这样解决了问题。但是呢,随着我们项目的扩大,线下卡顿的问题也越来越多,同时,在线上,也有卡顿的反馈,但是线上的反馈卡顿,我们在线下难以复现,于是我们开始寻找自动化的卡顿监测方案,其思路是来自于 Android 的消息处理机制,主线程执行任何代码都会回到 Looper.loop 方法当中,而这个方法中有一个 mLogging 对象,它会在每个 message 的执行前后都会被调用,我们就是利用这个前后处理的时机来做到的自动化监测方案的。同时,在这个阶段,我们也完善了线上 ANR 的上报,我们采取的方


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


式就是监控 ANR 的信息,同时结合了 ANR-WatchDog,作为高版本没有文件权限的一个补充方案。在做完这个卡顿检测方案之后呢,我们还做了线上监控及线下检测工具的建设,最终实现了一整套完善,多维度的解决方案。

5、你是怎么样自动化的获取卡顿信息?

我们的思路是来自于 Android 的消息处理机制,主线程执行任何代码它都会走到 Looper.loop 方法当中,而这个函数当中有一个 mLogging 对象,它会在每个 message 处理前后都会被调用,而主线程发生了卡顿,那就一定会在 dispatchMessage 方法中执行了耗时的代码,那我们在这个 message 执行之前呢,我们可以在子线程当中去 postDelayed 一个任务,这个 Delayed 的时间就是我们设定的阈值,如果主线程的 messaege 在这个阈值之内完成了,那就取消掉这个子线程当中的任务,如果主线程的 message 在阈值之内没有被完成,那子线程当中的任务就会被执行,它会获取到当前主线程执行的一个堆栈,那我们就可以知道哪里发生了卡顿。


经过实践,我们发现这种方案获取的堆栈信息它不一定是准确的,因为获取到的堆栈信息它很可能是主线程最终执行的一个位置,而真正耗时的地方其实已经执行完成了,于是呢,我们就对这个方案做了一些优化,我们采取了高频采集的方案,也就是在一个周期内我们会多次采集主线程的堆栈信息,如果发生了卡顿,那我们就将这些卡顿信息压缩之后上报给 APM 后台,然后找出重复的堆栈信息,这些重复发生的堆栈大概率就是卡顿发生的一个位置,这样就提高了获取卡顿信息的一个准确性。

6、卡顿的一整套解决方案是怎么做的?

首先,针对卡顿,我们采用了线上、线下工具相结合的方式,线下工具我们册中医药尽可能早地去暴露问题,而针对于线上工具呢,我们侧重于监控的全面性、自动化以及异常感知的灵敏度。


同时呢,卡顿问题还有很多的难题。比如说有的代码呢,它不到你卡顿的一个阈值,但是执行过多,或者它错误地执行了很多次,它也会导致用户感官上的一个卡顿,所以我们在线下通过 AOP 的方式对常见的耗时代码进行了 Hook,然后对一段时间内获取到的数据进行分析,我们就可以知道这些耗时的代码发生的时机和次数以及耗时情况。然后,看它是不是满足我们的一个预期,不满足预期的话,我们就可以直接到线下进行修改。同时,卡顿监控它还有很多容易被忽略的一个盲区,比如说生命周期的一个间隔,那对于这种特定的问题呢,我们就采用了编译时注解的方式修改了项目当中所有 Handler 的父类,对于其中的两个方法进行了监控,我们就可以知道主线程 message 的执行时间以及它们的调用堆栈。


对于线上卡顿,我们除了计算 App 的卡顿率、ANR 率等常规指标之外呢,我们还计算了页面的秒开率、生命周期的执行时间等等。而且,在卡顿发生的时刻,我们也尽可能多地保存下来了当前的一个场景信息,这为我们之后解决或者复现这个卡顿留下了依据。

7、TextView setText 耗时的原因,对 TextView 绘制层源码的理解?

8、开放问题:优化一个列表页面的打开速度和流畅性。


需要更全面更深入的理解请查看Android性能优化之绘制优化深入探索Android布局优化(上)深入探索Android布局优化(下)深入探索Android卡顿优化(上)深入探索Android卡顿优化(下)

5、App 瘦身

6、网络优化

1、移动端获取网络数据优化的几个点

  • 1、连接复用:节省连接建立时间,如开启 keep-alive。于 Android 来说默认情况下 HttpURLConnection 和 HttpClient 都开启了 keep-alive。只是 2.2 之前 HttpURLConnection 存在影响连接池的 Bug。

  • 2、请求合并:即将多个请求合并为一个进行请求,比较常见的就是网页中的 CSS Image Sprites。如果某个页面内请求过多,也可以考虑做一定的请求合并。

  • 3、减少请求数据的大小:对于 post 请求,body 可以做 gzip 压缩的,header 也可以做数据压缩(不过只支持 http 2.0)。


返回数据的 body 也可以做 gzip 压缩,body 数据体积可以缩小到原来的 30%左右(也可以考虑压缩返回的 json 数据的 key 数据的体积,尤其是针对返回数据格式变化不大的情况,支付宝聊天返回的数据用到了)。


  • 4、根据用户的当前的网络质量来判断下载什么质量的图片(电商用的比较多)。

  • 5、使用 HttpDNS 优化 DNS:DNS 存在解析慢和 DNS 劫持等问题,DNS 不仅支持 UDP,它还支持 TCP,但是大部分标准的 DNS 都是基于 UDP 与 DNS 服务器的 53 端口进行交互。HTTPDNS 则不同,顾名思义它是利用 HTTP 协议与 DNS 服务器的 80 端口进行交互。不走传统的 DNS 解析,从而绕过运营商的 LocalDNS 服务器,有效的防止了域名劫持,提高域名解析的效率。



参考文章

2、客户端网络安全实现

3、设计一个网络优化方案,针对移动端弱网环境。

7、App 电量优化

8、安卓的安全优化

1、提高 app 安全性的方法?

2、安卓的 app 加固如何做?

3、安卓的混淆原理是什么?

4、谈谈你对安卓签名的理解。

9、为什么 WebView 加载会慢呢?

这是因为在客户端中,加载 H5 页面之前,需要先初始化 WebView,在 WebView 完全初始化完成之前,后续的界面加载过程都是被阻塞的。


优化手段围绕着以下两个点进行:


  • 预加载 WebView。

  • 加载 WebView 的同时,请求 H5 页面数据。


因此常见的方法是:


  • 全局 WebView。

  • 客户端代理页面请求。WebView 初始化完成后向客户端请求数据。

  • asset 存放离线包。


除此之外还有一些其他的优化手段:


  • 脚本执行慢,可以让脚本最后运行,不阻塞页面解析。

  • DNS 链接慢,可以让客户端复用使用的域名与链接。

  • React 框架代码执行慢,可以将这部分代码拆分出来,提前进行解析。

10、如何优化自定义 View

为了加速你的 view,对于频繁调用的方法,需要尽量减少不必要的代码。先从 onDraw 开始,需要特别注意不应该在这里做内存分配的事情,因为它会导致 GC,从而导致卡顿。在初始化或者动画间隙期间做分配内存的动作。不要在动画正在执行的时候做内存分配的事情。


你还需要尽可能的减少 onDraw 被调用的次数,大多数时候导致 onDraw 都是因为调用了 invalidate().因此请尽量减少调用 invaildate()的次数。如果可能的话,尽量调用含有 4 个参数的 invalidate()方法而不是没有参数的 invalidate()。没有参数的 invalidate 会强制重绘整个 view。


另外一个非常耗时的操作是请求 layout。任何时候执行 requestLayout(),会使得 Android UI 系统去遍历整个 View 的层级来计算出每一个 view 的大小。如果找到有冲突的值,它会需要重新计算好几次。另外需要尽量保持 View 的层级是扁平化的,这样对提高效率很有帮助。


如果你有一个复杂的 UI,你应该考虑写一个自定义的 ViewGroup 来执行他的 layout 操作。与内置的 view 不同,自定义的 view 可以使得程序仅仅测量这一部分,这避免了遍历整个 view 的层级结构来计算大小。

11、FC(Force Close)什么时候会出现?

Error、OOM,StackOverFlowError、Runtime,比如说空指针异常


解决的办法:


  • 注意内存的使用和管理

  • 使用 Thread.UncaughtExceptionHandler 接口

12、Java多线程引发的性能问题,怎么解决

13、TraceView 的实现原理,分析数据误差来源。

14、是否使用过 SysTrace,原理的了解?

15、mmap + native 日志优化?

传统日志打印有两个性能问题,一个是反复操作文件描述符表,一个是反复进入内核态。所以需要使用 mmap 的方式去直接读写内存。

二、Android Framework 相关

1、Android 系统架构


Android 是一种基于 Linux 的开放源代码软件栈,为广泛的设备和机型而创建。下图所示为 Android 平台的五大组件:


1.应用程序


Android 随附一套用于电子邮件、短信、日历、互联网浏览和联系人等的核心应用。平台随附的应用与用户可以选择安装的应用一样,没有特殊状态。因此第三方应用可成为用户的默认网络浏览器、短信 Messenger 甚至默认键盘(有一些例外,例如系统的“设置”应用)。


系统应用可用作用户的应用,以及提供开发者可从其自己的应用访问的主要功能。例如,如果您的应用要发短信,您无需自己构建该功能,可以改为调用已安装的短信应用向您指定的接收者发送消息。


2、Java API 框架


您可通过以 Java 语言编写的 API 使用 Android OS 的整个功能集。这些 API 形成创建 Android 应用所需的构建块,它们可简化核心模块化系统组件和服务的重复使用,包括以下组件和服务:


  • 丰富、可扩展的视图系统,可用以构建应用的 UI,包括列表、网格、文本框、按钮甚至可嵌入的网络浏览器

  • 资源管理器,用于访问非代码资源,例如本地化的字符串、图形和布局文件

  • 通知管理器,可让所有应用在状态栏中显示自定义提醒

  • Activity 管理器,用于管理应用的生命周期,提供常见的导航返回栈

  • 内容提供程序,可让应用访问其他应用(例如“联系人”应用)中的数据或者共享其自己的数据


开发者可以完全访问 Android 系统应用使用的框架 API。


3、系统运行库


1)原生 C/C++ 库


许多核心 Android 系统组件和服务(例如 ART 和 HAL)构建自原生代码,需要以 C 和 C++ 编写的原生库。Android 平台提供 Java 框架 API 以向应用显示其中部分原生库的功能。例如,您可以通过 Android 框架的 Java OpenGL API 访问 OpenGL ES,以支持在应用中绘制和操作 2D 和 3D 图形。如果开发的是需要 C 或 C++ 代码的应用,可以使用 Android NDK 直接从原生代码访问某些原生平台库。


2)Android Runtime


对于运行 Android 5.0(API 级别 21)或更高版本的设备,每个应用都在其自己的进程中运行,并且有其自己的 Android Runtime (ART) 实例。ART 编写为通过执行 DEX 文件在低内存设备上运行多个虚拟机,DEX 文件是一种专为 Android 设计的字节码格式,经过优化,使用的内存很少。编译工具链(例如 Jack)将 Java 源代码编译为 DEX 字节码,使其可在 Android 平台上运行。


ART 的部分主要功能包括:


  • 预先 (AOT) 和即时 (JIT) 编译

  • 优化的垃圾回收 (GC)

  • 更好的调试支持,包括专用采样分析器、详细的诊断异常和崩溃报告,并且能够设置监视点以监控特定字段


在 Android 版本 5.0(API 级别 21)之前,Dalvik 是 Android Runtime。如果您的应用在 ART 上运行效果很好,那么它应该也可在 Dalvik 上运行,但反过来不一定。


Android 还包含一套核心运行时库,可提供 Java API 框架使用的 Java 编程语言大部分功能,包括一些 Java 8 语言功能。


4、硬件抽象层 (HAL)


硬件抽象层 (HAL) 提供标准界面,向更高级别的 Java API 框架显示设备硬件功能。HAL 包含多个库模块,其中每个模块都为特定类型的硬件组件实现一个界面,例如相机或蓝牙模块。当框架 API 要求访问设备硬件时,Android 系统将为该硬件组件加载库模块。


5、Linux 内核


Android 平台的基础是 Linux 内核。例如,Android Runtime (ART) 依靠 Linux 内核来执行底层功能,例如线程和低层内存管理。使用 Linux 内核可让 Android 利用主要安全功能,并且允许设备制造商为著名的内核开发硬件驱动程序。

对于 Android 应用开发来说,最好能手绘下面的系统架构图:

2、View 的事件分发机制?滑动冲突怎么解决?

了解 Activity 的构成

一个 Activity 包含了一个 Window 对象,这个对象是由 PhoneWindow 来实现的。PhoneWindow 将 DecorView 作为整个应用窗口的根 View,而这个 DecorView 又将屏幕划分为两个区域:一个是 TitleView,另一个是 ContentView,而我们平时所写的就是展示在 ContentView 中的。

触摸事件的类型

触摸事件对应的是 MotionEvent 类,事件的类型主要有如下三种:


  • ACTION_DOWN

  • ACTION_MOVE(移动的距离超过一定的阈值会被判定为 ACTION_MOVE 操作)

  • ACTION_UP


View 事件分发本质就是对 MotionEvent 事件分发的过程。即当一个 MotionEvent 发生后,系统将这个点击事件传递到一个具体的 View 上。

事件分发流程

事件分发过程由三个方法共同完成:


dispatchTouchEvent:方法返回值为 true 表示事件被当前视图消费掉;返回为 super.dispatchTouchEvent 表示继续分发该事件,返回为 false 表示交给父类的 onTouchEvent 处理。


onInterceptTouchEvent:方法返回值为 true 表示拦截这个事件并交由自身的 onTouchEvent 方法进行消费;返回 false 表示不拦截,需要继续传递给子视图。如果 return super.onInterceptTouchEvent(ev), 事件拦截分两种情况:


  • 1.如果该 View 存在子 View 且点击到了该子 View, 则不拦截, 继续分发


给子 View 处理, 此时相当于 return false。


  • 2.如果该 View 没有子 View 或者有子 View 但是没有点击中子 View(此时 ViewGroup


相当于普通 View), 则交由该 View 的 onTouchEvent 响应,此时相当于 return true。


注意:一般的 LinearLayout、 RelativeLayout、FrameLayout 等 ViewGroup 默认不拦截, 而 ScrollView、ListView 等 ViewGroup 则可能拦截,得看具体情况。


onTouchEvent:方法返回值为 true 表示当前视图可以处理对应的事件;返回值为 false 表示当前视图不处理这个事件,它会被传递给父视图的 onTouchEvent 方法进行处理。如果 return super.onTouchEvent(ev),事件处理分为两种情况:


  • 1.如果该 View 是 clickable 或者 longclickable 的,则会返回 true, 表示消费


了该事件, 与返回 true 一样;


  • 2.如果该 View 不是 clickable 或者 longclickable 的,则会返回 false, 表示不


消费该事件,将会向上传递,与返回 false 一样。


注意:在 Android 系统中,拥有事件传递处理能力的类有以下三种:


  • Activity:拥有分发和消费两个方法。

  • ViewGroup:拥有分发、拦截和消费三个方法。

  • View:拥有分发、消费两个方法。


三个方法的关系用伪代码表示如下:


public boolean dispatchTouchEvent(MotionEvent ev) {boolean consume = false;if (onInterceptTouchEvent(ev)) {consume = onTouchEvent(ev);} else {coonsume = child.dispatchTouchEvent(ev);}


return consume;}复制代码


通过上面的伪代码,我们可以大致了解点击事件的传递规则:对应一个根 ViewGroup 来说,点击事件产生后,首先会传递给它,这是它的 dispatchTouchEvent 就会被调用,如果这个 ViewGroup 的 onInterceptTouchEvent 方法返回 true 就表示它要拦截当前事件,接着事件就会交给这个 ViewGroup 处理,这时如果它的 mOnTouchListener 被设置,则 onTouch 会被调用,否则 onTouchEvent 会被调用。在 onTouchEvent 中,如果设置了 mOnCLickListener,则 onClick 会被调用。只要 View 的 CLICKABLE 和 LONG_CLICKABLE 有一个为 true,onTouchEvent()就会返回 true 消耗这个事件。如果这个 ViewGroup 的 onInterceptTouchEvent 方法返回 false 就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的 dispatchTouchEvent 方法就会被调用,如此反复直到事件被最终处理。

一些重要的结论:

1、事件传递优先级:onTouchListener.onTouch > onTouchEvent > onClickListener.onClick。


2、正常情况下,一个时间序列只能被一个 View 拦截且消耗。因为一旦一个元素拦截了此事件,那么同一个事件序列内的所有事件都会直接交给它处理(即不会再调用这个 View 的拦截方法去询问它是否要拦截了,而是把剩余的 ACTION_MOVE、ACTION_DOWN 等事件直接交给它来处理)。特例:通过将重写 View 的 onTouchEvent 返回 false 可强行将事件转交给其他 View 处理。


3、如果 View 不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,并且当前 View 可以持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。


4、ViewGroup 默认不拦截任何事件(返回 false)。


5、View 的 onTouchEvent 默认都会消耗事件(返回 true),除非它是不可点击的(clickable 和 longClickable 同时为 false)。View 的 longClickable 属性默认都为 false,clickable 属性要分情况,比如 Button 的 clickable 属性默认为 true,而 TextView 的 clickable 默认为 false。


6、View 的 enable 属性不影响 onTouchEvent 的默认返回值。


7、通过 requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。


记住这个图的传递顺序,面试的时候能够画出来,就很详细了:


ACTION_CANCEL 什么时候触发,触摸 button 然后滑动到外部抬起会触发点击事件吗,再滑动回去抬起会么?

  • 一般 ACTION_CANCEL 和 ACTION_UP 都作为 View 一段事件处理的结束。如果在父 View 中拦截 ACTION_UP 或 ACTION_MOVE,在第一次父视图拦截消息的瞬间,父视图指定子视图不接受后续消息了,同时子视图会收到 ACTION_CANCEL 事件。

  • 如果触摸某个控件,但是又不是在这个控件的区域上抬起(移动到别的地方了),就会出现 action_cancel。

点击事件被拦截,但是想传到下面的 View,如何操作?

重写子类的 requestDisallowInterceptTouchEvent()方法返回 true 就不会执行父类的 onInterceptTouchEvent(),即可将点击事件传到下面的 View。

如何解决 View 的事件冲突?举个开发中遇到的例子?

常见开发中事件冲突的有 ScrollView 与 RecyclerView 的滑动冲突、RecyclerView 内嵌同时滑动同一方向。


滑动冲突的处理规则:


  • 对于由于外部滑动和内部滑动方向不一致导致的滑动冲突,可以根据滑动的方向判断谁来拦截事件。

  • 对于由于外部滑动方向和内部滑动方向一致导致的滑动冲突,可以根据业务需求,规定何时让外部 View 拦截事件,何时由内部 View 拦截事件。

  • 对于上面两种情况的嵌套,相对复杂,可同样根据需求在业务上找到突破点。


滑动冲突的实现方法:


  • 外部拦截法:指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。具体方法:需要重写父容器的 onInterceptTouchEvent 方法,在内部做出相应的拦截。

  • 内部拦截法:指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。具体方法:需要配合 requestDisallowInterceptTouchEvent 方法。


加深理解,GOGOGO

3、View 的绘制流程?

DecorView 被加载到 Window 中

  • 从 Activity 的 startActivity 开始,最终调用到 ActivityThread 的 handleLaunchActivity 方法来创建 Activity,首先,会调用 performLaunchActivity 方法,内部会执行 Activity 的 onCreate 方法,从而完成 DecorView 和 Activity 的创建。然后,会调用 handleResumeActivity,里面首先会调用 performResumeActivity 去执行 Activity 的 onResume()方法,执行完后会得到一个 ActivityClientRecord 对象,然后通过 r.window.getDecorView()的方式得到 DecorView,然后会通过 a.getWindowManager()得到 WindowManager,最终调用其 addView()方法将 DecorView 加进去。

  • WindowManager 的实现类是 WindowManagerImpl,它内部会将 addView 的逻辑委托给 WindowManagerGlobal,可见这里使用了接口隔离和委托模式将实现和抽象充分解耦。在 WindowManagerGlobal 的 addView()方法中不仅会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 通过 root.setView()把 DecorView 加载到 Window 中。这里的 ViewRootImpl 是 ViewRoot 的实现类,是连接 WindowManager 和 DecorView 的纽带。View 的三大流程均是通过 ViewRoot 来完成的。

了解绘制的整体流程

绘制会从根视图 ViewRoot 的 performTraversals()方法开始,从上到下遍历整个视图树,每个 View 控件负责绘制自己,而 ViewGroup 还需要负责通知自己的子 View 进行绘制操作。

理解 MeasureSpec

MeasureSpec 表示的是一个 32 位的整形值,它的高 2 位表示测量模式 SpecMode,低 30 位表示某种测量模式下的规格大小 SpecSize。MeasureSpec 是 View 类的一个静态内部类,用来说明应该如何测量这个 View。它由三种测量模式,如下:


  • EXACTLY:精确测量模式,视图宽高指定为 match_parent 或具体数值时生效,表示父视图已经决定了子视图的精确大小,这种模式下 View 的测量值就是 SpecSize 的值。

  • AT_MOST:最大值测量模式,当视图的宽高指定为 wrap_content 时生效,此时子视图的尺寸可以是不超过父视图允许的最大尺寸的任何尺寸。

  • UNSPECIFIED:不指定测量模式, 父视图没有限制子视图的大小,子视图可以是想要的任何尺寸,通常用于系统内部,应用开发中很少用到。


MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包的方法,打包方法为 makeMeasureSpec,解包方法为 getMode 和 getSize。


普通 View 的 MeasureSpec 的创建规则如下:



对于 DecorView 而言,它的 MeasureSpec 由窗口尺寸和其自身的 LayoutParams 共同决定;对于普通的 View,它的 MeasureSpec 由父视图的 MeasureSpec 和其自身的 LayoutParams 共同决定。

如何根据 MeasureSpec 去实现一个瀑布流的自定义 ViewGroup?

View 绘制流程之 Measure

  • 首先,在 ViewGroup 中的 measureChildren()方法中会遍历测量 ViewGroup 中所有的 View,当 View 的可见性处于 GONE 状态时,不对其进行测量。

  • 然后,测量某个指定的 View 时,根据父容器的 MeasureSpec 和子 View 的 LayoutParams 等信息计算子 View 的 MeasureSpec。

  • 最后,将计算出的 MeasureSpec 传入 View 的 measure 方法,这里 ViewGroup 没有定义测量的具体过程,因为 ViewGroup 是一个抽象类,其测量过程的 onMeasure 方法需要各个子类去实现。不同的 ViewGroup 子类有不同的布局特性,这导致它们的测量细节各不相同,如果需要自定义测量过程,则子类可以重写这个方法。(setMeasureDimension 方法用于设置 View 的测量宽高,如果 View 没有重写 onMeasure 方法,则会默认调用 getDefaultSize 来获得 View 的宽高)

getSuggestMinimumWidth 分析

如果 View 没有设置背景,那么返回 android:minWidth 这个属性所指定的值,这个值可以为 0;如果 View 设置了背景,则返回 android:minWidth 和背景的最小宽度这两者中的最大值。

自定义 View 时手动处理 wrap_content 时的情形

直接继承 View 的控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent。此时,可以在 wrap_content 的情况下(对应 MeasureSpec.AT_MOST)指定内部宽/高(mWidth 和 mHeight)。

LinearLayout 的 onMeasure 方法实现解析(这里仅分析 measureVertical 核心源码)

系统会遍历子元素并对每个子元素执行 measureChildBeforeLayout 方法,这个方法内部会调用子元素的 measure 方法,这样各个子元素就开始依次进入 measure 过程,并且系统会通过 mTotalLength 这个变量来存储 LinearLayout 在竖直方向的初步高度。每测量一个子元素,mTotalLength 就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上的 margin 等。

在 Activity 中获取某个 View 的宽高

由于 View 的 measure 过程和 Activity 的生命周期方法不是同步执行的,如果 View 还没有测量完毕,那么获得的宽/高就是 0。所以在 onCreate、onStart、onResume 中均无法正确得到某个 View 的宽高信息。解决方式如下:


  • Activity/View#onWindowFocusChanged:此时 View 已经初始化完毕,当 Activity 的窗口得到焦点和失去焦点时均会被调用一次,如果频繁地进行 onResume 和 onPause,那么 onWindowFocusChanged 也会被频繁地调用。

  • view.post(runnable): 通过 post 可以将一个 runnable 投递到消息队列的尾部,始化好了然后等待 Looper 调用次 runnable 的时候,View 也已经初始化好了。

  • ViewTreeObserver#addOnGlobalLayoutListener:当 View 树的状态发生改变或者 View 树内部的 View 的可见性发生改变时,onGlobalLayout 方法将被回调。

  • View.measure(int widthMeasureSpec, int heightMeasureSpec):match_parent 时不知道 parentSize 的大小,测不出;具体数值时,直接 makeMeasureSpec 固定值,然后调用 view..measure 就可以了;wrap_content 时,在最大化模式下,用 View 理论上能支持的最大值去构造 MeasureSpec 是合理的。

View 的绘制流程之 Layout

首先,会通过 setFrame 方法来设定 View 的四个顶点的位置,即 View 在父容器中的位置。然后,会执行到 onLayout 空方法,子类如果是 ViewGroup 类型,则重写这个方法,实现 ViewGroup 中所有 View 控件布局流程。

LinearLayout 的 onLayout 方法实现解析(layoutVertical 核心源码)

其中会遍历调用每个子 View 的 setChildFrame 方法为子元素确定对应的位置。其中的 childTop 会逐渐增大,意味着后面的子元素会被放置在靠下的位置。


注意:在 View 的默认实现中,View 的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于 View 的 measure 过程,而最终宽/高形成于 View 的 layout 过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。在一些特殊的情况下则两者不相等:


  • 重写 View 的 layout 方法,使最终宽度总是比测量宽/高大 100px。

  • View 需要多次 measure 才能确定自己的测量宽/高,在前几次测量的过程中,其得出的测量宽/高有可能和最终宽/高不一致,但最终来说,测量宽/高还是和最终宽/高相同。

View 的绘制流程之 Draw

Draw 的基本流程

绘制基本上可以分为六个步骤:

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
【收藏】2021年Android跳槽大厂必备宝典(1),android教程零基础入门