IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 面试系列(一)Android 基础 -> 正文阅读

[移动开发]面试系列(一)Android 基础

文章目录

序言

博客很久没更新了,上次更新的时候还没有毕业 =_=!后来由于工作和自身的原因,一直懒得没有维护博客。今年年后,自己也是准备跳槽找工作,于是复习整理了一下 Android 相关的基础知识,都是自己辛苦看过、总结、码出来的,希望对其他找工作的同孩儿们能有一些帮助 ~

在这里插入图片描述

后续也会抽时间陆续整理一下其他的问题,比如 Java 相关的知识、源码分析、面试过程中遇到的问题、心得和感受等等,觉得写得不错的观众老爷记得点赞加收藏哦 ~

四大组件

Activity

Activity 是 Android 中四大组件部分我们接触的最多,也最常使用的一个组件了,由此大家也应该知道这块的重要性了吧,相对于其他组件来说,这块也是问的最多的~
在这里插入图片描述

Activity 生命周期

pic

正常流程是:onCreate() -> onStart() -> onResume() -> onPause() -> onStop() -> onDestroy()

当 Activity 被其他的透明 Activity 盖住时,只会执行其 onPause 方法,因为 Activity 只是被遮住了,失去焦点,但对用户还可见。但当 Activity 被 Dialog 盖住时,并不会执行任何生命周期方法!!!网上大多数博客说的都是错误的!!!

onStart() 与 onResume() 区别?

  • onStart() 是 Activity 界面被显示出来的时候执行的,此时还不能与其交互
  • onResume() 是 Activity 与用户能进行交互时执行,用户可以获取其焦点。

Activity 启动模式

这部分是 Activity 组件的重中之重,也是面试中最常问到的问题,可能平时我们都觉得自己掌握的差不多了,但真的问起来,你,确定你答的上吗?
在这里插入图片描述

launchMode

在这里插入图片描述

  • standrad:系统默认的启动模式,多次启动同一个 Activity,Activity 栈中可存在多个实例

  • singleTop:栈顶复用,若 A 在栈顶,则再次启动 A 时,会调用 A 的 onNewIntent() 方法,而不会再启动一个 A 入栈;若 A 不在栈顶,再次启动 A 时,会重新入栈一个 A Activity。

  • singleTask:栈内唯一。A 设置了 singleTask,若 A 在栈顶,与 singleTop 效果一样;若 A 不在栈顶,则会将 A 之上的所有 Activity 出栈(singleTask 默认附带 CLEAR_TOP 效果),然后回调 A 的 onNewIntent 方法。

  • 启动一个设置了 singleTask 启动模式的 Activity,在启动这个 Activity 时,会寻找这个 Activity 所在的栈是否存在,存在则直接入栈,否则创建一个新栈,将 Activity 入栈。若在这个 Activity 中再启动别的 Activity,则默认情况下,这个 Activity 入的是这个栈,不是以前 App 的栈。
  • 若在 App1 中启动了 App2 中设置了 singleTask 的 Activity(假设为 A),则 A 会寻找自己所属的栈,压入其中,若没有指定 taskAffinity,则 A 会加入 App2 的栈,然后将 App2 整个栈拿过来,压在 App1 的栈上(有 task 间的切换动画)。若一步步返回的时候,则会基于 App2 的栈一步步退出,直到整个 App2 都退出了,会切换回 App1 跳转 A 之前的页面(task 间切换的动画)。
  • task 的叠加只适用于前台 task。task 由前台进入后台,会被拆开。比如:1)当按了 Home;2)按了最近任务键(在最近任务列表显示出来的时候就已经进入后台,而不是在切换到其他应用之后),再回来,叠加的 task 就会被拆开。
  • singleInstance:跟 singleTask 有些像,但是设置了 singleInstance 的 Activity,会独自占用一个栈。
    比如有 A、B、C 三个 Activity。B 设置了 singleInstance,则 A 启动 B 时,B 会 new 一个新栈并入栈,再从 B 启动 C 时,C 会压入 A 的栈。

在这里插入图片描述

那么问题来了,MainActivity 一般用什么启动模式呢?

使用 Intent 标记

可以使用 Intent 标记覆盖在 Activity 中设置的 launchMode

  • FLAG_ACTIVITY_NEW_TASK:同 singleTask,但是 singleTask 实际上还附带 FLAG_ACTIVITY_CLEAR_TOP Flag
  • FLAG_ACTIVITY_SINGLE_TOP:同 singleTop
  • FLAG_ACTIVITY_CLEAR_TOP:如果要启动的 Activity 已经在当前任务中运行,则不会启动该 Activity 的新实例,而是会销毁位于它之上的所有其他 Activity,并通过 onNewIntent() 将此 intent 传送给它的已恢复实例(现在位于堆栈顶部)。

FLAG_ACTIVITY_CLEAR_TOP 最常与 FLAG_ACTIVITY_NEW_TASK 结合使用,达到 singleTask 的效果。

taskAffinity

可翻译为栈亲和性。一个 Activity 不设置 taskAffinity,默认与 Application 的 taskAffinity 相同,Application 默认的 taskAffinity 是 packageName。可以通过这个属性与 singleTask 配合让 Activity 在一个新栈中打开。

一个 task 的 taskAffinity 取自栈底 Activity 的 taskAffinity。

正常情况下一个 Application 只会在多任务列表中显示一个 task,因为 当多个 task 具有相同的 taskAffinity 时,最近任务列表里只会显示最新展示过的那个。如果为一个 App 的多个 Activity 设置不同的 taskAffinity,则可以在多任务列表下看到这些 Activity 的缩略。如果为这些 Activity 分别配置了 action 为 MAIN、category 为 LAUNCH,以及 label 和 icon,则会在应用程序列表中看到这些 Activity 的快捷打开方式。

清除返回栈

如果用户离开任务较长时间,系统默认会清除任务中除根 Activity 以外的所有 Activity。当用户再次返回到该任务时,只有根 Activity 会恢复。系统之所以采取这种行为方式是因为,经过一段时间后,用户可能已经放弃了之前执行的操作,现在返回任务是为了开始某项新的操作。可以使用一些 Activity 属性改变此行为:

  • alwaysRetainTaskState:如果在任务的根 Activity 中将该属性设为 “true”,则不会发生上述默认行为。即使经过很长一段时间后,任务仍会在其堆栈中保留所有 Activity。
  • clearTaskOnLaunch:如果在任务的根 Activity 中将该属性设为 “true”,那么只要用户离开任务再返回,堆栈就会被清除到只剩根 Activity。
  • finishOnTaskLaunch:该属性与 clearTaskOnLaunch 类似,但它只会作用于单个 Activity 而非整个任务。它还可导致任何 Activity 消失,包括根 Activity。如果将该属性设为 “true”,则 Activity 仅在当前会话中归属于任务。如果用户离开任务再返回,则该任务将不再存在。
allowTaskReparenting

允许 task 重新回去属于自己的栈。如下:对 Activity 设置 allowTaskReparenting = “true”。为 App2 的 A Activity 设置了这个属性,在 App1 中启动 App2 的 A Activity,A 会被压入到 App1 的栈中。回到桌面,再打开 App2 时,会把 A Activity 拉回到 App2 自己的栈中。

使用 Intent 隐式启动 Activity(IntentFilter 匹配规则)
  • 实例化 Intent
  • 为 Intent 设置 action,若目标 Activity 设置了 action,则此处设置一个匹配任意 action 的 action 过去
  • 为 Intent 设置 category,可以不设置,不设置默认为 Intent.CATEGORY_DEFAULT。
  • 为 Intent 设置 data,若目标 Activity 设置了 data,则此处设置一个匹配任意 data 的 data 过去
    • URI 匹配规则:<scheme>://<hose>:<port>/[<path>|<pathPrefix>|<pathPattern>]
    • 如:content://com.example.project:200/folder/etc
    • 若未指定 URI,默认值(schema)为 content 和 file
  • 需要调用 Intent#setDataAndType 方法,单独设置 data 会将 type 清空;单独设置 type 会将 data 清空。

Activity 的启动流程

一些关键类

  • Instrumentation:监控应用与系统相关的交互行为
  • AMS:组件管理调度中心
  • ActivityStarter:Activity 启动的控制器,处理 Intent 与 Flag 对 Activity 启动的影响。
    1.寻找符合启动条件的 Activity,如果有多个,让用户选择;
    2.校验启动参数合法性;
    3.返回 int 参数,代表 Activity 是否启动成功
  • ActivityStack:用来管理任务栈里的 Activity
  • ActivityStackSupervisior:用来管理任务栈。高版本才有的类,用来管理多个 ActivityStack,早期的版本只有一个 ActivityStack 对应着手机屏幕,后来高版本支持多屏之后就有了多个 ActivityStack,于是就引入了 ActivityStackSupervisior 来管理多个 ActivityStack
  • ApplicationThread:实际起作用的类,是 ActivityThread 的内部类,Activity、Service、BroadcastReceiver 的启动、切换、调度等各种操作都在这个类中完成。
  1. startActivity 最终会调用到 startActivityForResult 方法。
  2. startActivityForResult 方法中会调用 Instrumentation#execStartActivity 方法,将调用委托给 ActivityManagerProxy
  3. ActivityManagerProxy 会调用 system_server 进程中的 AMS 的 startActivity 方法
  4. AMS 会将调用委托给 ApplicationThreadProxy(AMS 最终会调用 ActivityStackSupervisor 中的 realStartActivityLocked 方法,通过 ApplicationThreadProxy,最终调用到 ApplicationThread 的 scheduleLaunchActivity 方法。)
  5. ApplicationThreadProxy 将发调用委托给 ApplicationThread 的 scheduleLaunchActivity 方法,给 ActivityThread 中的 Handler 发送消息,处理启动 Activity 的 launch 请求
  6. 按顺序调用 Activity 中的 handleLaunchActivity 方法,performResumeActivity() 方法等。完成 Activity 的启动。

Activity 的启动流程

当点击一个应用图标以后,都发生了什么,描述一下这个过程?

点击应用图标后会去启动应用的 LauncherActivity,如果 LauncherActivity 所在的进程还没有创建,会先创建进程,整体的流程就是一个 Activity 的启动流程。

Activity 启动流程图

  1. 点击桌面应用图标,Launcher 进程将启动 Activity 的请求以 Binder 的形式发送给了 AMS
  2. AMS 收到请求后,交给 ActivityStarter 处理 Intent 和 Flag 等信息,然后交给 ActivityStackSupervisior 和 ActivityStack 处理 Activity 进栈相关流程,同时请求 Zygote 进程 fork 新进程
  3. Zygote 接收到新进程创建请求后,fork 出新进程
  4. 在新进程里创建 ActivityThread 对象,调用其 main 方法,创建 Looper,开启消息循环。
  5. ActivityStackSupervisior 中将启动 Activity 的指令通过 ApplicationThreadProxy 代理给 ActivityThread 中的 ApplicationThread 中的 scheduleLaunchActivity 方法。
  6. 通过 Handler 发送消息给 ActivityThread 中的 Handler,执行 handleLaunchActivity、performResumeActivity 等方法,回调 Activity 对应的生命周期。

基于 Android 9.0(API 28) 的 Activity 启动流程分析

ActivityStackSupervisior 负责所有 Activity 栈的管理,内部管理了 mHomeStack、mFocusedStack 和 mLastFocusedStack 三个 Activity 栈。mHomeStack 管理的是 Launcher 相关的 Activity 栈;mFocusedStack 管理的是当前显示在前台 Activity 的 Activity 栈;mLastFocusedStack 管理的是上一次显示在前台 Activity 的 Activity 栈。

  1. startActivity 或 startActivityForResult 向 AMS 发起 startActivity 的启动请求。
  2. AMS 收到请求后,最终会调用到 startActivityAsUser 方法,构建一个 ActivityStarter 对象,调用其 execute 方法,最终调用到 startActivityUnchecked 方法,在 startActivityUnchecked 方法中会处理 Activity 的 Intent 信息、Flag 信息,判断 Activity 启动模式、是否要调用 deliverNewIntent 回调 onNewIntent 等。然后交给 ActivityStackSupervisior 和 ActivityStack 处理 Activity 进栈相关流程(ActivityStarter#startActivityUnChecked -> ActivityStackSupervisor#resumeFocusedStackTopActivityLocked)
  3. ActivityStackSupervisior 中会调用 mFocusedStack 的 resumeTopActivityUncheckedLocked 方法,判断是否有 Activity 处于 Resume 状态,有的话会先让这个 Activity 进入 Pausing 过程,然后 ActivityStackSupervisior 再执行 startSpecificActivityLocked 尝试启动要启动的 Activity (ActivityStackSupervisior#resumeTopActivityInnerLocked -> ActivityStack#startPausingLocked -> ActivityStack#startSpecificActivityLocked)
  4. 执行完 resume 的 Activity 的 pause之后,ActivityStackSupervisior 中会判断当前进程是否创建(startSpecificActivityLocked),未创建,则会调用 Zygote fork 出一个新进程,并通过反射创建 ActivityThread,执行它的 main 方法完成主线程的初始化
  5. ActivityThread 的 main 方法中会创建 Looper 并开启 loop 循环。还会调用 attach 方法,通过 AMS 为应用绑定 Application(关联 ApplicationThread) 对象。(然后添加一个垃圾回收观察者,每当系统触发垃圾回收,会在 run 方法中计算应用使用了多少内存,如果超过总量的四分之三就会释放内存)
  6. 主线程和主进程都初始化完毕后,attachApplication 的最后会调用 ActivityStackSupervisor 的 realStartActivityLocked 方法,启动 Activity(通过 ClientLifecycleManager.scheduleTransaction post 了一个 LaunchActivityItem),(ActivityStackSupervisor#realStartActivityLocked),最后会执行到 ActivityThread 中启动 Activity 的相关流程
  7. 执行之前栈顶 Activity 的 onStop 过程

Android 9.0 引入了 ClientLifecycleManager 和 ClientTransactionHandler 辅助管理 Activity 生命周期。ActivityThread 继承自 ClientTransactionHandler,ClientTransactionHandler 通过调用 ActivityThread 的 sendMessage 方法向 ActivityThread 中的 Handler 发送消息。它会发送 EXECUTE_TRANSACTION 消息到 ActivityThread.H 里继续处理。

  • ClientLifecycleManager.scheduleTransaction -> ClientTransaction.schedule -> ApplicationThread.scheduleTransaction -> ClientTransactionHandler.scheduleTransaction -> 发送消息给 ActivityThread.H

Android 系统启动流程是什么(提示:init 进程 -> Zygote 进程 -> SystemServer 进程 -> 各种系统服务 -> 应用进程)?

  1. 启动电源以及系统启动:当电源按下时引导芯片从预定义的地方(固化在ROM)开始执行,加载引导程序BootLoader到RAM,然后执行。
  2. 引导程序BootLoader:BootLoader是在Android系统开始运行前的一个小程序,主要用于把系统OS拉起来并运行。
  3. Linux内核启动:当内核启动时,设置缓存、被保护存储器、计划列表、加载驱动。当其完成系统设置时,会先在系统文件中寻找init.rc文件,并启动init进程。
  4. init进程启动:初始化和启动属性服务,并且启动Zygote进程。
  5. Zygote进程启动:创建JVM并为其注册JNI方法,创建服务器端Socket,启动SystemServer进程。
  6. SystemServer进程启动:启动Binder线程池和SystemServiceManager,并且启动各种系统服务。
  7. Launcher启动:被SystemServer进程启动的AMS会启动Launcher,Launcher启动后会将已安装应用的快捷图标显示到系统桌面上。

Service

服务并不是运行在一个单独的进程中,而是创建于创建服务时所在的应用程序进程。Service 默认不会开启线程,若要处理耗时操作,需要自己开启工作线程。使用时需要先在 AndroidManifest 中注册服务,然后在组件中开启服务。

启动方法

  • startService:开启 Service,与调用者生命周期无关,调用者退出后仍然运行
  • bindService:绑定 Service,与调用者生命周期相关,调用者退出后也随之退出

生命周期

  • 只是调用 startService() 启动服务:onCreate()->onStartCommand()->onDestroy()
  • 只是调用 bindService() 启动服务:onCreate()->onBind()->onUnBind()->onDestroy()
  • 同时使用 startService() 和 bindService():onCreate()->onStartCommand()->onBind()->onUnBind()->onDestroy()

Activity 与 Service 通信

使用 bindService 绑定服务。调用 bindService 方法时,需要传入一个实现了 ServiceConnection 接口的对象,在 onServiceConnected 方法中可以获取到 Service 中的 onBind 方法返回的 IBinder 对象,就可以进行 Activity 与 Service 间通信了。

也可以在 Service 中创建一个 Messenger 对象,传入一个 Handler 作为参数。在 onServiceConnected 中拿到 Messenger 对象,通过 send 方法发送消息,交给实例化 Messenger 时传入的 Handler 对象处理。

  • 服务只能被创建一次,onCreate 方法只会被回调一次,之后调用 startService 方法只会回调 onStartCommand 方法
  • 若一个 Service 既调用了 startService,又调用了 bindService 方法,则必须调用了 stopService 和 unbindService 两个方法时才会被 destroy
  • onStartCommand 方法返回值 START_STICKY,表示当服务由于异常被 kill 时,若情况允许系统会自动重启服务,但是重启后,传入的 intent 对象为 null;START_REDELIVER_INTENT 时,系统会重启服务并传入 intent。
  • BroadcastReceiver 中不能使用 bindService,因为 bindService 生命周期随着组件,而广播执行完 onReceive 中代码就销毁了

IntentService

HandlerThread

HandlerThread 就是一个封装了 Looper 的 Thread 类。

  • 在重写的 run 方法中加了锁,保证线程安全的获取当前线程的 Looper 对象,获取成功之后通过 notifyAll 方法唤醒其他线程。
  • 在 getLooper 方法获取当前线程 Looper 时加锁,若 looper 对象为 null 时调用 wait 等待。等 run 方法中创建好 looper 对象后再返回 looper 对象。
IntentService 内部机制
  • 使用了 HandlerThread 实现,在 onCreate 方法中会实例化一个 HandlerThread() 对象,并且使用 HandlerThread 对象的 Looper 对象构造一个 Handler 对象 mServiceHandler。
  • 通过 mServiceHandler 发送的消息,都会在 HandlerThread 中执行。
  • 每次通过 onStartCommand 方法传过来的 Intent 对象,都会 通过 handler 的 handleMessage 方法回调到 onHandleIntent 方法中,同一时刻只传递一个 Intent 对象。
  • 所有请求都执行完之后,会调用 stopSelf 方法自动停止服务。
为什么在 mServiceHandler 的 handleMessage() 回调方法中执行完 onHandlerIntent() 方法后要使用带参数的 stopSelf() 方法?

因为 stopSelf() 会立即停止服务,而 stopSelf(int startid) 会等所有消息都处理完之后才终止服务。一般情况下,带参数的 stopSelf 方法在尝试停止服务之前,会判断最近启动服务的次数是否和 startid 相等。相等就立刻停止服务,不相等则不停止。

为什么 bindService 可以跟 Activity 生命周期联动

  1. bindService 执行时,LoadedApk 会记录 ServiceConnection 信息
  2. Activity 执行 finish 方法时,会通过 LoadedApk 检查 Activity 是否存在未注销/解绑的 BroadcastReceiver 和 ServiceConnection,如果有,会通知 AMS 注销/解绑对应的 BroadcastReceiver 和 Service,并打印异常信息,通知用户应主动进行注销/解绑。

如何保证 Service 不被杀死?

提高进程优先级,降低进程被杀死的概率
  1. 监控手机锁屏解锁事件,在屏幕锁屏时启动 1 个像素的 Activity,将当前进程提升为前台进程,减少被系统杀死的概率。在解锁时,将 Activity 销毁
  2. 启动前台 Service(在 onStartCommand 中调用 startForeground() 方法把 Service 提升为前台进程,在 onDestroy 中调用 stopForeground() 方法)
  3. 提升 Service 优先级
    在 AndroidManifest.xml 文件中为 Service 添加 节点,在 intent-filter 中添加 android:priority = "1000"设置最高优先级,1000 是最高值,数组越小优先级越低,广播同样适用
在进程被杀死后,进行拉活
  1. 注册高频的系统事件的广播接收器,如网络变化、解锁屏幕、开机等
  2. 双进程互相唤起
  3. 在 Service 的 onDestroy 中发送一个自定义广播,收到广播时,重启 Service
依靠第三方

根据终端不同,MIUI 系统接入小米推送,华为手机接入华为推送等

BroadcastReceiver

应用场景

  • 普通广播:sendBroadcast() 发送,异步执行,广播发出后,所有注册的广播接收器几乎会同时受到消息,没有先后顺序,最常用的广播
  • 有序广播:sendOrderedBroadcast(),发送出去的广播会同步执行,广播接收者按 priority 属性从大到小排序执行,priority 属性相同的,动态注册的广播优先,广播接收者还可以在高优先级的广播接收者中进行截断和修改广播。
  • 本地广播:LocalBroadcastManager,本地广播发送的广播只能在应用程序内部进行传递,并且广播接收器也只能接受来自本应用程序的广播。利用 Handler实现,利用了 IntentFilter 的 match 功能,提供消息的发布与接收功能,效率比较高。本地广播不能静态注册。

注册方式

  • 静态注册:常驻系统,不受组件生命周期影响,即便应用退出,广播还是可以被接收,耗电,占内存。
  • 动态注册:非常驻,跟随组件生命周期变化,组件结束,广播结束。在组件结束前,需要先移除广播,否则容易造成内存泄露。

发送和接收的原理

  1. 继承 BroadcastReceiver,重写 onReceive 方法
  2. 通过 Binder 机制向 AMS 注册广播
  3. 通过 Binder 机制向 AMS 发送广播
  4. AMS 查找符合相应条件的广播,通过 IntentFilter/Permission 匹配,将广播发送到 BroadcastReceiver 所在的消息队列中
  5. 广播所在消息队列拿到广播后,回调它的 onReceiver() 方法

传输数据的限制

  1. 广播是通过 Intent 携带需要传递的数据的
  2. Intent 是通过 Binder 机制实现的
  3. Binder 对数据的大小有限制,不同 Rom 不一样,一般为 1M

ContentProvider、ContentResolver

简介

  • ContentProvider:管理数据,提供数据的增删改查操作,数据源可以是数据库、文件、XML 等,ContentProvider 为数据的访问提供了统一接口,可以用来做进程间数据共享。
  • ContentResolver:ContentResolver 可以通过不同的 URI 操作不同的 ContentProvider 中的数据,外部进程可以通过 ContentResolver 与 ContentProvider 进行交互。
  • ContentObserver:观察 ContentProvider 中的数据变化,并将变化通知给外界。

ContentProvider 包括六个抽象方法,分别是:onCreate、query、update、delete、insert 和 getType。onCreate 是指 ContentProvider 的创建,一般初始化的工作在这里做。getType 用来返回一个 Uri 请求所对应的 MIME 类型。

ContentProvider 初始化过程

ContentProvider 的 onCreate 方法是在 Application 的 attachBaseContext 和 onCreate 之间调用的,所以很多第三方库利用了这个特点,在 ContentProvider 中去完成库初始化,这样接入方就不需要显示的在 Application 中实例化三方库。

ContentProvider 对应方法所在的线程

onCreate 由系统调用,并运行在主线程里,其它五个方法均由外界回调并运行在 Binder 线程池中。

Fragment

Fragment 的生命周期与 Activity 之间的关系

在这里插入图片描述

生命周期

当调用了 FragmentTransaction#add 方法将一个 Fragment 添加到一个容器中时,Fragment 会按顺序回调如下方法

  • onAttach(Context context):onAttach() 方法会在 Fragment 与 Activity 窗口关联后立刻调用。从该方法开始,就可以通过 getActivity() 方法获取到与 Fragment 关联的 Activity 窗口对象了。但此时 Fragment 中的 View 还未初始化,所以不能操纵控件。
  • onCreate(Bundle savedInstanceState):可以在 Bundle 对象中获取一些在 Activity 中传递过来的数据,通常会在该方法中读取保存的这状态,获取或初始化一些数据。不要进行耗时操作,不然 Activity 窗口不会显示
  • onCreateView(LayoutInfalter inflater,ViewGroup container,Bundle savedInstanceState):很重要的方法,在该方法中,创建 Fragment 显示的 View,saveInstanceState 可以获取 Fragment 保存的状态
  • onViewCreated(View view,Bundle savedInstanceState):view 参数是上一步创建好的,在 onCreateView 之后调用,在任何 saved state 恢复到 View 之前。子类可以在这里初始化自己的一些 view 相关的内容。
  • onActivityCreated(Bundle saveInstanceState):在 Activity 的 onCreate() 方法执行完之后,会立即调用这个方法,表示 Activity 已经初始化完成
  • onStart():同 Activity
  • onResume():同 Activity

当调用了 FragmentTransaction#hide() 方法隐藏一个 Fragment 时

不会回调生命周期方法

当调用了 FragmentTransaction#remove 方法将一个 Fragment 移除时,Fragment 会按顺序回调如下方法

  • onPause
  • onStop
  • onDestroyView():onCreateView() 中的 View 将被移除
  • onDestroy
  • onDetach():Fragment 与 Activity 不再有关联。

FragmentTransaction#replace()

等效于

  • 先调用 FragmentTransaction#remove()
  • 在调用 FragmentTransaction#add()

FragmentTransaction#addToBackStack 方法

addToBackStack() 保存的是一系列针对一个fragmentTransaction的操作记录,按照回退栈 add 的 Fragment,按 back 键后会一级级回调回去。

FragmentTransaction#beginTransaction()
    .add(R.id.container, fragment1, Fragment1::class.java.simpleName)
    .addToBackStack("a")
    .commit()
回退栈常见的方法
  1. FragmentTransaction#addToBackStack(String name):将一个刚添加的 Fragment 加入到回退栈
  2. getSupportFragmentManager().getBackStackEntryCount():获取回退栈中的数量
  3. getSupportFragmentManager().popBackStack():弹出栈顶 fragment
  4. getSupportFragmentManager().popBackStack(String name,int flags):根据 name 立刻弹出栈顶 fragment
  5. getSupportFragmentManager().popBackStack(int id,int flags):根据 id 立刻弹出栈顶 fragment

Fragment 的通信

  1. Fragment 可以通过 getActivity() 拿到 Activity 对象实例
  2. Activity 中可以通过为 Fragment 设置接口,监听 fragment,通过接口传数据给 Activity
  3. Activity 可以通过 Bundle 传数据给 Fragment
  4. 可以使用 ViewModel 共享数据

FragmentPagerAdapter 与 FragmentStatePagerAdapter 的区别

  • FragmentPagerAdapter 适用于页面较少的情况,destroyItem() 方法中只是调用了 FragmentTransaction 的 detach 方法。
  • FragmentStatePagerAdapter 适用于页面较多的情况,每次切换 ViewPager 的时候是回收内存的。destroyItem 调用了 FragmentTransaction 的 remove 方法。

遇到过哪些 Fragment 的问题,如何处理的?

Fragment 视图重叠

Activity 因为异常被杀死,恢复界面的时候,Fragment 页面重叠了。

  • 在 onSaveInstanceState 方法中,通过 FragmetnManager#putFragment(Bundle outState,String key,Fragment fragment) 方法将当前展示的 Fragment 存储到 bundle 中。
  • 在调用 FragmentTransaction#add 方法添加 Fragment 时,添加一个 tag
  • 在 onCreate 中,先通过 FragmentManager#findFragmentByTag(String tag) 来找对应 tag 的 Fragment 是否存在,不为空则使用旧的 Fragment
  • 然后判断 saveInstanceState 是否为空,不为空则取出当前应该展示的 Fragment 展示
getActivity() 空指针

一般发生在异步任务里调用 getActivity() 方法,而 Fragment 已经 onDetach() 了,此时就会空指针。可以维护一个全局变量 mActivity,在 onAttach() 中赋值,同时也要记得在 Fragment 销毁时,要停掉所有异步任务。

  • 在 Fragment 中与子 Fragment 交互,用 getChildFragmentManager() 方法获取 FragmentManager
  • 在 Fragment 中不要使用 getActivity().startActivityForResult(),会回调到 Activity 的 onActivityResult 方法中;要直接使用 startActivityForResult() 方法。

Window

Window 是一个抽象类,Activity、Dialog、Toast、PopupWindow 等都是依附于 Window 对象的。Winndow 的实际实现类是 PhoneWindow 类。

Window 的类型

ViewRootImpl 中的 Window 默认使用 WindowManager#LayoutParams(),而 WindowManager#LayoutParams 默认是 MATCH_PARENT,并且 type 是 TYPE_APPLICATION 类型的。Window 有三种类型:

  • Application Window
  • Sub Window
  • System Window

Window 的添加过程

  • 需要通过 WindowManager 的 addView 实现,最终 addView 方法会委托给 WindowManagerGlobal 中的 addView,会实例化一个 ViewRootImpl 对象,并调用它的 setView 方法。
  • WindowManagerGlobal 中有一个 list,存储了 Window 上的所有 View、所有 ViewRootImpl 和所有布局参数信息。
  • ViewRootImpl 中会调用 requestLayout 触发布局过程。
  • Window 的添加是一次 IPC 过程,通过 binder 机制,调用 IWindowSession 的 addToDisPlay 方法,将 Window 的添加请求交给 WMS 处理。IWindowSession 的实现类是 Session,且在 Session#addToDisPlay 方法中会对 Window 的 token 和 type 进行检查,Dialog 使用 Application 的 Context 创建时报错信息就是在这里检查出来报错的。

Window 的删除过程

删除过程和添加过程一样,都是先通过 WindowManagerImpl 后,调用委托给 WindowManagerGlobal 对象实现。WindowManagerGlobal#removeView 方法。

  • 通过 findViewLocked 查找待删除的 View 索引
  • 调用 removeViewLocked 通过 ViewRootImpl 来做进一步删除
  • WindowManager 中提供了两种删除接口,removeView 和 removeViewImmediate,异步和同步的删除,一般都是使用异步的
  • ViewRootImpl#die 方法 -> doDie() 方法
    • 垃圾回收相关工作
    • 通过 Session 的 remove 方法删除 Window(IPC 过程,会调用 WMS 的 removeWindow 方法)
    • 调用 View 的 dispatchDetachedFromWindow 方法,内部会调用 View 的 onDetachedFromWindow()
      • 当 View 从 Window 中移除时,这个方法会调用,可以在这里做一些资源回收工作,如终止动画、停止线程
    • 调用 WindowManagerGlobal 的 doRemoveView 方法刷新数据

Window 与 WindowManager

  • Window 通过 Window#setWindowManager 与 WindowManager 关联到一起
  • 通过 WindowManager#addView 将 DecorView 与 WindowManager 关联到一起
  • Activity 的 attach 方法执行时,会创建 PhoneWindow 类型的 Window 对象,并且调用 setWindowManager 将 Window 与 WindowManager 关联起来。在 performResumeActivity 中,通过调用 WindowManager#addView,将 Window 对象中的 DecorView 与 wm 关联起来。
  • Dialog 同理,是在构造函数中创建 Window 对象并将 Window 与 WindowManager 关联在一起。在 show 方法时,调用 wm#addView,将 decorview 放上去显示。

View 相关

View 基础知识

View 的位置参数

  • View 的位置主要由它的四个顶点决定,left、top、right、bottom。(left,top) 是左上角顶点,(right,bottom) 是右下角顶点。
  • 这些坐标都是相对于 View 的父容器来说的,也就是相对坐标
  • width = right - left/height = bottom - top,在 onLayout 之后可以获得
  • getRawX、getRawY 是相对于屏幕左上角的坐标

Android 3.0 之后新增了几个参数,x、y、translationX 和 translationY。x、y 是 View 的左上角,translationX 和 translationY 是 View 左上角相对于父容器的偏移量,默认是 0。

  • View 在平移的过程中,top 和 left 表示的是原始左上角的位置信息,是不会发生改变的,发生改变的是 x、y、translationX 和 translationY 四个参数。
  • x = left + translationX
  • y = top + translationY

MotionEvent 和 TouchSlop

MotionEvent

Android 中所有的事件都是针对 MotionEvent 的。通过它可以获取到各种跟手指相关的交互。

getAction() 与 getActionMask()
  • getAction 用于单点触控,只能获取到 ACTION_DOWN 等单点触控事件
  • getActionMask 用于多点触控获取事件,可以获取到包括 ACTION_POINTER_DOWN 在内的多点触控事件
事件
  • ACTION_DOWN:第一根手指按下
  • ACTION_UP:最后一根手指抬起
  • ACTION_POINTER_DOWN:非第一根治按下
  • ACTION_POINTER_UP:非最后一根手指抬起
常用方法
  • getActionIndex(index):获取非第一根按下手指或非最后一根抬起手指的 index
  • getPointerId(actionIndex):根据 actionIndex 获取手指的 pointerId,每根手指的 index 可能会变,但只要它未抬起,pointerId 是不会变的
  • findPointerIndex(pointerId):根据 pointerId 寻找 index
TouchSlop

TouchSlop 是系统能识别出的被认为是滑动的最小距离。是一个常量,跟设备相关,使用这个常量可以帮助处理滑动。比如当两次滑动事件的距离小于这个数,就不处理等。

ViewConfiguration.get(context).getScaledTouchSlop()

VelocityTracker、GestureDetector

VelocityTracker

速度追踪,用于追踪手指在滑动过程中的速度,包括水平速度和垂直方向的速度。使用如下,先在 onTouchEvent 方法中追踪当前单击事件的速度:

VelocityTracker vt = VelocityTracker.obtain();
vt.addMovement(event)

获取当前速度:

vt.computeCurrentVelocity(1000); // 时间间隔,单位 ms
int xVelocity = (int) vt.getXVelocity();
int yVelocity = (int) vt.getYVelocity();
...
// 不使用了之后要重置并回收
vt.clear();
vt.recycle();
  • 使用之前先计算速度,即 getXVelocity/getYVelocity 之前需要先调用 computeCurrentVelocity 方法。
  • 速度指一段时间内手指一动的像素数,公式为:速度 = (终点位置 - 起点位置)/ 时间段
  • 向着坐标系正方向,速度为正,向着坐标系负方向,速度为负。
  • 不使用了之后要重置并回收
GestureDetector

手势检测。可用于在点击、长按之外,监听双击、滑动、放大缩小等手势

  • 声明一个 GestureDetector 类,传入一个实现了 OnGestureListener 接口的类,在 OnGestureListener 类里完成手势监听的处理。一般也可以用 SimpleOnGestureListener 来实现 OnGestureListener。
  • 接管目标 View 的 onTouchEvent 方法,在待监听 View 的 onTouchEvent 方法中返回 mGestureDetector.onTouchEvent(event)
  • OnGestureListener 中几个重要方法
    • onDown:手指轻触屏幕的瞬间,由一个 ACTION_DOWN 触发
    • onSingleTapUp:响应单击事件(轻轻触摸后松开),伴随一个 ACTION_UP 触发
    • onScroll:滑动事件
    • onFling:快速滑动,按下屏幕,快速滑动后松开
    • onLongPress:长按事件
  • OnDoubleTapListener:双击监听器
    • onDoubleTap:双击事件,不可能和 onSingleTapConfirmed 共存
    • onSingleTapConfirmed:单击
    • onDoubleTapEvent:表示发生了双击行为,双击期间,ACTION_DOWN、ACTION_MOVE 和 ACTION_UP 都会触发这个方法
ScaleGestureDetector

监听放大缩小(捏撑操作)

OnScaleGestureListener:监听类,在回调里处理自己的逻辑

View 的滑动

使用 View#scrollTo、View#scrollBy 函数

scrollBy 本质上调用的也是 scrollTo 函数,scrollTo 滑动到的是绝对位置,scrollBy 滑动到的是相对于之前的位置的偏移。

  • mScrollX 和 mScrollY 单位都是像素
  • 往 x、y 轴的正向滑动时,mScrollX 和 mScrollY 为负数
  • 往 x、y 轴的负向滑动时,mScrollX 和 mScrollY 为正数

注意,不管是 scrollTo 还是 scrollBy,滑动的都是 View 中的内容,不是 View 容器本身的位置的改变。本质上 scrollTo 方法是对 View 可视区域的滑动,可以理解为 layout 的 l、t、r、b 的区域的改变

Scroller 弹性滑动

弹性滑动对象,用于实现 View 的弹性滑动。当我们使用 View 的 scrollTo 和 scrollBy 分发进行滑动时,过程是瞬间完成的,没有过渡效果。使用 Scroller 可以使 View 实现动画效果的滑动。

Scroller 本身无法让 View 弹性滑动,需要配合 View 的 computeScroll 方法使用,共同完成这个功能。

示例代码:

// XXXView.java
...
private val scroller = Scroller(context)
fun smoothScroll(destX: Int, destY: Int) {
    val startX = scrollX
    val deltaX = destX - startX
    scroller.startScroll(startX, 0, destX, 0, 1000)
    invalidate()
}

override fun computeScroll() {
    if (scroller.computeScrollOffset()) {
        scrollTo(scroller.currX, scroller.currY)
        postInvalidate()
    }
}
...
  • 调用 Scroller#startScroller 方法时,只会记录一些关键参数,不会执行其他操作,需要调用 invalidate 触发 View 失效,让 View 重绘
  • View 的 onDraw 方法中会调用 computeScroll 方法,专门用于处理滑动的情况,父类中此方法是空实现,需要在自定义的子 View 中重写该方法
  • 在 computeScroll 中,需要调用 Scroller#computeScrollOffset 方法,返回 true,则证明滑动未执行完,否则证明滑动执行完了
  • computeScrollOffset 方法中会根据时间完成度算出当前 View 的 currentX 和 currentY 值,调用完 computeScrollOffset 之后,可以通过 Scroller#getCurrX() 和 Scroller#getCurrY() 来获取当前的 x、y 坐标,通过 scrollTo 传入参数执行滑动
  • 若 computeScrollOffset 返回为 true,别忘了继续调用 invalidate 方法再次触发重绘

Scroller 只会移动 View 的内容,并非 View 本身位置的改变

使用动画

View 的事件分发机制

View 的事件分发机制主要通过 dispatchTouchEvent()、onInterceptTouchEvent() 和 onTouchEvent() 三个方法完成,是否消费事件,主要取决于 ACTION_DOWN 事件是否返回 true。

  • dispatchTouchEvent():返回 true,表示事件被当前 View 消费;返回 super.dispatchTouchEvent() 表示继续分发该事件,返回 false 表示交给父 View 的 onTouchEvent 处理
  • onInterceptTouchEvent():返回 true,表示拦截事件,交给自己的 onTouchEvent 处理,返回 false 不拦截,继续传递给子 View。返回 super.onInterceptTouchEvent() 分两种情况
    • 该 ViewGroup 存在子 View 并且点击到了子 View 上,不拦截,继续分发给子 View 处理,相当于 return false(默认不拦截)
    • 该 ViewGroup 不存在子 View,或者存在子 View,但是没点中,交给自己的 onTouchEvent 处理,相当于 return true
  • onTouchEvent():返回 true,自己处理事件,返回 false,交给父 View 的 onTouchEvent 处理;返回 super.onTouchEvent(),两种情况
    • 该 View 是 clickable 或 longclickable 的,会返回 true,消费事件,自己处理
    • 该 View 不是 clickable 或 longclickable 的,返回 false,向上传递事件

一些重要结论

  1. 事件传递优先级,onTouchListener.onTouch > onTouchEvent > onClickListener.onClick
  2. 一个 View 的 onTouchEvent 返回 true 拦截了 ACTION_DOWN 事件,那么后续的所有事件都会继续发送给它,并且不再调用父 View 的 onTouchEvent
  3. 一个 View 调用 onInterceptTouchEvent 截获了某次 ACTION_DOWN 后,那么后续的事件不再经过 onInterceptTouchEvent 方法,因为 dispatchTouchEvent 中判断了当前不是 ACTION_DOWN,或者当前没有字 View 处理事件时,intercepted 直接置为 true,不走 onInterceptTouchEvent 方法
  4. 正常情况下,一个事件序列只能被一个 View 截获并消费
  5. 如果 View 不消耗除 ACTION_DOWN 以外的事件,这个点击事件会消失,父元素的 onTouchEvent 不会被调用,并且当前 View 可以收到这个事件序列后续的事件,这些消失的事件都会传递给 Activity 处理
  6. ViewGroup 默认不拦截任何事件
  7. View 的 enable 不影响 onTouchEvent 的默认返回值
  8. requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外,因为每次调用 dispatchTouchEvent 时,都会清除之前的状态,其中就包括了 FLAG_DISALLOW_INTERCEPT 的 flag

事件分发流程

Android 中事件分发的流程其实就是个责任链模式,都是先从 Activity 开始,分发到顶层 View,顶层 View 再一层层递归调用子 View,这是分发的过程。处理的过程就是从子 View 一层层向上回调。流程如下所述

事件分发机制伪代码:

public boolean dispatchTouchEvent(MotionEvent ev){
    boolean consume = false;
    if(onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev);  
    } else {
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}
  • 一个 ACTION_DOWN 事件,先从 Activity 的 dispatchTouchEvent() 分发开始向上传递,Activity 的 dispatchTouchEvent 分发中,会调用 window 对象的 superDispatchTouchEvent 方法。此处若所有的 View 都不处理事件,则交由 Activity 的 onTouchEvent 方法自己处理。若处理,则因为 window 的实际实现类是 PhoneWindow,从 PhoneWindow 又向上传递最终会调用到顶层 View 也就是 PhoneView 中的 DecorView 中,DecorView 是 FrameLayout 的子类,则最终调用的就是 ViewGroup 的 dispatchTouchEvent 方法
  • ViewGroup 中的 dispatchTouchEvent 方法,会先判断是否是一个 ACTION_DOWN 事件,当前是否有子 View 处理事件。
    • 若满足其一(DOWN 事件,或有子 View 处理事件),调用 onInterceptTouchEvent 判断是否拦截事件
    • 若不是 ACTION_DOWN 并且没有子 View 处理事件,则 intercepted 直接置为 true,后续事件不调用 onInterceptTouchEvent 方法。ViewGroup 自己处理此事件序列的事件
  • ViewGroup 不拦截事件,遍历 Children,判断手指点击落在哪个 Child 上
    • 找到了 Child,调用它的 dispatchTouchEvent 方法
      • Child 自己处理事件,将其赋值给 mFirstTouchTarget,breadk 跳出 for 循环
      • Child 自己不处理事件,dispatchTouchEvent 返回 false
    • 没找到 Child,继续往下走(ViewGroup 存在子 View,但是没点中子 View 的情况,走自己的 onTouchEvent 方法,onInterceptTouchEvent 相当于返回了 true)
  • 判断 mFirstTouchTarget 若为空,证明没有 Child 处理事件,ViewGroup 自己处理事件,调用 super.dispatchTouchEvent
  • mFirstTouchTarget 不为空,判断当前 target 中是否有处理过事件,也就是上面有 Child 处理了事件的情况下,handle 标记为 true
  • 否则走正常事件传递流程,调用子 View 的 dispatchTouchEvent 方法

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

  • ACTION_CANCEL 和 ACTION_UP 一般都作为 View 的一段事件处理的结束。如果在父 View 中拦截 ACTION_UP 或 ACTION_MOVE,在第一次父 View 拦截消息的瞬间,父 View 指定了子 View 不接受后续消息了,同时子 View 会收到 ACTION_CANCEL 事件
  • 如果触摸某个控件,但是又不在这个控件的区域上抬起(移动出了 View 的范围),会出现 ACTION_CANCEL

点击事件被拦截,但是想传递到下边的 View 怎么办?

调用 view.requestDisallowInterceptTouchEvent(true) 请求父 View 不要拦截

如何处理滑动冲突?

滑动冲突,有几种情况,一种是指容器和子 View 的滑动方向相同时的冲突;一种是容器与子 View 的滑动方向不同时的冲突;更复杂的情况是上述两种掺杂在一起的时候,不过可以通过分别处理外层与中间层,中间层与内层的关系解决。针对第一种、第二种,典型的有 ScrollView 嵌套 RecyclerView、ViewPager 嵌套 RecyclerView 等。

  • 对于滑动方向不一致的滑动冲突,可以通过多种方式界定是否要拦截,如:判断某个方向的滑动距离大于另一个方向;判断某个方向的滑动速度大于另一个方向;或者判断滑动时与水平方向的夹角。
  • 对于滑动方向一致的滑动冲突,可以通过业务需求规定何时需要拦截

滑动冲突有两种解决方式:

  • 外部拦截法:比较符合 Android 中事件的分发机制,在父 View 的 onInterceptTouchEvent 中根据业务需求,判断父容器需要处理此事件,则拦截,否则不拦截。
  • 内部拦截法:稍微复杂,当子 View 需要某个事件时,配合调用 requestDisallowInterceptTouchEvent 方法请求父容器不要拦截,在子 View 中自行处理,否则交由父容器处理。

绘制流程

View 的绘制流程(DecorView 是如何与 WindowManager 关联到一起的?/setContentView 是如何将 View 添加到视图上的?)(基于 API 24)

  • ActivityThread 中执行到 preformLaunchActivity 时,会实例化 Activity 对象,之后会执行 Activity 的 attach 方法,在 Activity 的 attach 方法中会实例化 Window 对象、WindowManager 对象,接着会调用 Activity 的 onCreate 方法
  • Activity 的 onCreate 方法中会调用 setContentView 将 View 设置进去,setContentView 实际上调用的是 window 对象的 setContentView 方法,而 window 的实际实现类是 PhoneWindow,在 PhoneWindow 的 setContentView 方法中会创建顶层 DecorView 对象,这就是最顶层的 View。PhoneWindow 会根据 Activity 的主题等加载一个布局到 DecorView,一般是包含一个 Title 和一个 Content,content 是一个 FrameLayout,取出这个 FrameLayout,赋值给 mContentParent 对象,最后将我们设置进去的 View inflate 出来 add 到这个 FrameLayout 中。
  • 在 ActivityThread 中的 handleResumeActivity 方法中,会取出上一步实例化的 window 对象、windowManager 对象,以及 decorView 对象,然后执行 windowManager 的 addView 方法。WindowManager 的实现类是 WindowManagerImpl,WindowManagerImpl 中又将真正的实现委托给了 WindowManagerGlobal
  • WindowManagerGlobal 中的 addView 方法中实例化了一个 ViewRootImpl 对象,并且调用了它的 setView 方法,将 View 传递进去,WindowManager 与 DecorView 就关联到一起了
  • setView 方法中会 requestLayout,最终执行 View 绘制流程的地方在 performTraversals 方法中,依次执行测量、布局和绘制。

DecorView 的 MeasureSpec 由窗口尺寸和其自身的 LayoutParams 共同决定;普通 View,它的 MeasureSpec 由父 View 的 MeasureSpec 和其自身的 LayoutParams 共同决定

Measure

  • 自定义 View 时,如果需要自己测量宽高,需要重写 onMeasure 方法,如果是 ViewGroup,则在 onMeasure 中完成所有子 View 的测量(如果子 View 是 ViewGroup,则会递归这个过程),然后结合子 View 宽高、父 View 测量出来的自己的宽高,将最终结果通过 setMeasureDimension 设置进去
  • 在 ViewGroup 中,measureChildren(measureChildWithMargins) 方法会遍历测量 ViewGroup 中所有的 View,当 View 的可见性处于 GONE 时,不会测量
  • 测量某个子 View 时,需要传入父 View 的 MeasureSpec,结合子 View 自身的 LayoutParams 参数可以计算出子 View 的 MeasureSpec,然后调用子 View 的 measure 方法,子 View 的 onMeasure 方法,测量自身,将测量结果写入 mMeasureWidth、mMeasureHeight
  • setMeasureDimension 方法用于设置测量出来的宽高,如果 View 没有重写 onMeasure 方法,则默认的实现中,会传入 getDefaultSize 来获取 View 的宽高
  • getDefaultSize 的参数是 getSuggestMinimumWidth 方法,该方法会判断 View 是否有背景,如没有,则取 minWidth 属性,默认为 0,;如果设置了背景,则返回 minWidth 和背景最小宽度中的最大值
  • 直接继承 View,若不在 onMeasure 中处理 AT_MOST 也就是 WRAP_CONTENT 的情况,则默认 WRAP_CONTENT 与 MATCH_PARENT 的行为一致。因为在 View 的 onMeasure 方法中,getDefaultSize 中,AT_MOST 和 EXACTLY 的行为是一样的。取父 View 允许使用的最大值。
  • 测量子 View 大小时,需要根据父 View 的 MeasureSpec 规格,加上子 View 自己的 LayoutParams 生成一个子 View 的 MeasureSpec,然后调用 child.measure() 方法测量子 View。子 View MeasureSpec 的生成可以看 ViewGroup 中的 getChildMeasureSpec() 方法。
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

Layout

  • layout 方法确定 View 本身的位置,onLayout 方法确定所有子 View 的位置
  • View 的 layout 方法会通过 setFrame 方法来设定 View 的四个顶点的位置(setFrame 方法中还会调用 onSizeChanged 回调),即子 View 在父 View 中的位置(left、top、right、bottom)。接着会回调 onLayout 方法,让父 View 确定子 View 的位置,如果是 ViewGroup,则需要实现这个方法,实现 ViewGroup 中所有 View 控件的布局流程。
layout 的作用
  • 我们在 Measure 过程中获取到子 View 的 measureWidth 和 measureHeight,在 Layout 过程中对子 View 进行摆放
  • Measure 过程通过设置 PFLAG_LAYOUT_REQUIRED 标记告诉 View 需要进行 onLayout,而 Layout 过程通过清除 PFLAG_FORCE_LAYOUT 告诉 Measure 过程不需要执行 onMeasure 了
  • View 的绘制需要 Canvas,Canvas 是有作用域限制的
    • 对于硬件加速绘制来说,通过 Layout 过程中设置的 RenderNode 坐标
protected boolean setFrame(int left, int top, int right, int bottom) {
    ...
    mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
    ...
}
* 对于软件绘制来说
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    canvas.translate(mLeft - sx, mTop - sy);
}
getMeasuredWidth() 与 getWidth 的区别
  • getMeasuredWidth 在 measure 执行过后,调用过 setMeasureDimensions 就可以获取到,得到的是测量的宽;
  • getWidth 需要在 onLayout 之后才能获取到,是通过 mRight - mLeft 得来。是 View 真实的宽
  • getMeasuredWidth 与 getWidth 一般情况下最后都是相等的
何时可以获取真实的宽、高
  • 重写 View.onSizeChanged() 方法获取
  • 注册 View.addOnLayoutChangeListener(),在 onLayoutChange 里获取
  • 重写 onLayout 方法获取

onDraw

Draw 的基本步骤:

  1. 绘制 View 背景
  2. 按需选择是否 saveLayer
  3. 绘制 View 内容
  4. 绘制 View 的子 View,通过 dispatchDraw 传递绘制过程
  5. 按需绘制 View 的 fading 边缘,并 restoreLayer
  6. 绘制 View 的装饰,如滚动条

一些问题

ViewGroup 会调用 onDraw 吗?为什么?
  • ViewGroup 默认不会调用 onDraw,因为在 ViewGroup 的构造中,调用了 setFlags(WILL_NOT_DRAW, DRAW_MASK),等效于调用了 setWillNotDraw 方法,将其设置为了不可绘制的,等于将其标志为透明的。因为 ViewGroup 一般不需要绘制,所以系统为了优化,将其标记为不可绘制。在 View#draw 方法中,会判断当前标记位,是不透明的,才会去调用 onDraw。
  • 可以通过调用 setWillNotDraw(true),让 ViewGroup 可以执行 onDraw
private void initViewGroup() {
    // ViewGroup doesn't draw by default
    if (!debugDraw()) {
        setFlags(WILL_NOT_DRAW, DRAW_MASK);
    }
    ...
}
getWidth 和 getMeasureWidth 方法的区别?
  • getWidth 方法需要等 layout 过程结束之后才能拿到结果,是 View 的 mRight - mLeft 的结果
  • getMeasureWidth 方法在测量完成就可以拿到结果,是测量阶段的宽(View 需要调用 setMeasuredDimension 方法)
  • 一般情况下最终的 getMeasureWidth(测量过程中可能会经常改变,因为有的 ViewGroup 会执行多次测量流程) 和 getWidth 是相等的
onMeasure 有时候会执行多次?
  • 父 View 可以使用 UNSPECIFIED dimensions 来将它的每个子 View 都测量一次来算出它们到底需要多大尺寸,如果所有这些子 View 没被限制的尺寸(如 WRAP_CONTENT)的和太大或太小,那么它会用精确数值再次调用 measure() 方法。(父 View 的 onMeasure 方法中可能多次调用 child.measure() 方法)
  • 在 Window 根 View 和 Window 自身的测量上,即 ViewRootImpl 的 performTraversals 中,measure 过程也可能被执行多次。
    • measureHierarchy 方法中,会根据 DecorView 尺寸和 Window 的尺寸,去计算 RootView 的尺寸,最多计算三次
      • 如果与 Window 关联的 DecorView 宽度是 WRAP_CONTENT,并且传入的期望宽大于系统预置的 baseSize,则会根据 base 重新测量一次宽
      • 如果还是太小摆放不下,则会扩大 baseSize,重新测量
      • 还是不能满足要求,使用期望的宽高进行测量
    • measureHieracrchy 后边还会执行一次 measure 流程
如何在 Activity 启动时获取到 View 宽高?

View 的 measure 过程和 Activity 的生命周期方法不是同步执行的,如果 View 还未测量完,则在 onCreate、onStart、onResume 中都不能获取到 View 的宽高信息。可以通过如下几种方式解决:

  • 在 Activity/View 的 onWindowFocusChanged 方法中:此时 View 已经初始化完毕,当 Activity 的窗口获得焦点和失去焦点均会被调用,如果频繁执行 onResume 和 onPause,此方法会被调用多次
  • 调用 view.post 方法,在 runnable 中获取 View 的宽高。详情见下边解释
  • 获取 View 的 ViewTreeObserver:view.getViewTreeObserver().addonGlobalLayoutListener:当 View 树的状态发生改变,比如 View 可见性改变等,会触发 onGlobalLayout 方法,会被触发多次,记着移除
    • ViewTreeObserver#dispatchOnGlobalLayout() 是在 ViewRootImpl 的 performTraversals 中执行完 performLayout 之后执行的,所以可以获取到 View 宽高,但是此时绘制任务还未开始
  • 自己调用 view.measure() 方法测量:需要分情况处理,且 match_parent 时测不出。
为什么能在 View#post 中获取到 View 宽高?
  • 调用 View 的 post 方法时,如果 attachInfo 不为空,则直接调用 attachInfo 中的 handler 的 post 方法将消息发送出去,attachInfo 不为空时,View 的三大流程已经执行完了,这时可以获取到 View 的宽高。
  • 另一种情况是 attachInfo 为空时,比如在 onCreate 中调用 View#post 方法,此时 View 的三大流程还未执行完,会将 Runnable 消息 post 到一个 HandlerActionQueue 中等待执行。当 ViewRootImpl 调用 View 的 dispatchAttachedToWindow 方法时,会将 HandlerActionQueue 中的 runnable 消息真正的 post 出去,放到消息队列尾部,当执行到它们时,View 的测量已经完成,所以可以拿到 View 的宽高。
聊聊对子线程不能更新 UI 的看法?

子线程不能更新 UI,本质上是因为在与 ViewRootImpl 创建时的线程不是同一个线程的时候,不允许更新 UI。正常情况下 ViewRootImpl 是在 ActivityThread 中的 handleResumeActivity 中调用 WM#addView 方法时,在 WindowManagerGlobal 中创建的,此时是主线程。当在子线程更新 UI 时,会一层层传递到最终的 ViewRootImpl 中的 requestLayout 方法里,requestLayout 方法中会对线程做检查,若不等,抛异常。

如何在子线程更新 UI?

一种思路是规避掉 ViewRootImpl#requestLayout 中的线程检查机制;一种是创建一个新的 ViewRootImpl 环境。

  • 在 ViewRootImpl 未创建成功之前更新,也就是在 onCreate 中更新 UI
  • 开启硬件加速,并为 View 设置固定宽高,此时重绘 View 会跳过 checkThread 流程,走硬件加速的绘制流程
  • 在 SurfaceView 中可以拿到一个 canvas 对象,可以直接在其他线程更新 UI
  • 在子线程创建一个 ViewRootImpl 对象(WindowManager#addView 方法中会创建一个 ViewRootImpl),然后更新 UI
View#post 传入的 Runnable 一定会被执行吗?

不一定,若在 post 出去的 Runnable 执行之前,调用了 remove 方法,将 Runnable 移除调了,那么就不会执行,而且 API 不同版本区别如下:

  • API 24 以下不一定,API 24 以下,在 ViewRootImpl 中使用了 ThreadLocal 存储 HandlerActionQueue,如果在 ViewRootImpl 创建出来之前,在子线程 post 出去一个消息,ViewRootImpl 将永远拿不到这个消息
  • API 24 以上一定会执行,API 24 以上去掉了 ThreadLocal 存储 HandlerActionQueue 的设计,在 performTraversals 方法中会调用 View 的 dispatchAttachedToWindow 方法,里边会使用 attachInfo 中的 handler 将消息都 post 出去执行
requestLayout、invalidate、postInvalidate 的区别与联系?
  • 他们都能起到刷新 UI 的效果
  • invalidate 和 postInvalidate 方法只会调用 onDraw 方法进行重绘,requestLayout 会重新调用 onMeasure、onLayout,以及有可能调用 onDraw
  • 调用了 requestLayout 方法后,会为 View 添加一个 FLAG_FORCE_LAYOUT 标记,这个标记是用来表示在 measure 方法中判断是否要执行 onMeasure 的。这个标记会在 layout 结束时清除。同时递归调用父 View 的 requestLayout 方法。最终到 ViewRootImpl 的 requestLayout 方法触发 performTraversal 处理该事件,将 mLayoutRequested 标记为 true。出发 measure 和 layout,如果 layout 过程中发现 l、t、r、b 跟之前不同,就会触发 invalidate,也就是会触发 onDraw
  • invalidate 和 postinvalidate 相同,只不过 postinvalidate 将消息 post 到主线程执行。invalidate 会为该 View 添加一个标记位,同时递归调用父 View 的 invalidateChildInParent 方法,直到传递到 ViewRootImpl 中,最终触发 performTraversals。由于 mLayoutRequested 为 fasle,所以不会导致 onMeasure 和 onLayout 的调用,onDraw 会被调用

自定义 View

自定义 View 的几种类型

  • 继承自 View,重写 onDraw 方法,一般适用于一些特殊不规则的效果,在 onDraw 函数中自己处理绘制(如:下边圆形的计时 View)
  • 继承自特定的 ViewGroup,如 FrameLayout,处理组合 View 的情况。比如 XXTitleView,将某几个 View 组合成一个自定义 View,在里边处理 View 固有的逻辑,接入开发者不需要关心内部实现,直接接入使用即可。
  • 继承自某个 View,如:继承自 TextView 等,一般用于扩展某种已有 View 的功能
  • 继承 ViewGroup,重写 onLayout。一般适用于实现某些特殊的布局,如:下边的 TagLayout

Android 的数据存储

Android 中的数据存储一般包括文件存储、SharedPreferences 存储、数据库存储和 ContentProvider 共享数据

文件存储(I/O)

文件存储分为内部存储和外部存储。外部存储又分为私有外部存储和公共外部存储。访问本 App 内部的数据不需要访问权限,访问其他部分的,如 sdcard 中的某个文件/夹需要读写权限。

  • 从内部存储空间访问:getFilesDir() 或 getCacheDir() 方法,不需要任何权限。文件目录为 data/data/package_name/xxx
    • Context 的方法
    • App 删除时,内容会被删除
  • Context#getExternalFilesDir:
    • 从 API 19 之后,对此方法返回的路径上的文件不需要读写权限。但是只适用于当前调用 App 的包名路径下的文件
    • 访问别的 package 下的文件,还是需要 android.Manifest.permission#WRITE_EXTERNAL_STORAGEandroid.Manifest.permission#READ_EXTERNAL_STORAGE 权限
    • App 删除时,内容也会删除
    • App 相关的一些资源可以使用 getExternalFilesDir,放在 Application 对应目录下
    • 文件目录为 sdcard/Android/data/package_name/files/xxx,sdcard 对应 storage/emulated/0/...
  • Environment#getExternalStoragePublicDirectory:外部公共目录,操作需要读写权限,所有 Application 均可访问
    • 文件目录为 sdcard/Music(Pictures)...,sdcard 对应 storage/emulated/0/...
    • 不随 App 删除而删除

SharedPreferences

适用于保存少量的数据,比如一些配置信息等。核心原理是将数据的键值对保存到一个 xml 文件中。Sp 对象本身只能用于获取数据,需要调用 SharedPreferences#edit() 方法获取一个 Editor 对象用于 commit。

  • Sp 的 get 最终都会调用到 ContextImpl#getSharedPreferences(File file,int mode) 方法
  • ContextImpl 中会先去 ArrayMap 中找对应 file 的 sp 实例,找不到会构建一个 SharedPreferencesImpl 对象,并根据文件放入 ArrayMap
  • sp 的创建过程是线程安全的
  • SharedPreferencesImpl 的构造函数中,会开启一个线程去异步加载磁盘数据,并解析文件,将 key-value 对保存在 mMap(HashMap) 中。
  • 还会保存文件的修改时间戳(mStatTimestamp)和大小(mStatSize),用于跨进程的情况
  • 调用 notifyAll() 方法唤醒其他等待线程数据加载完毕
  • 如果 sp 文件过大,则会导致卡顿

sp 文件存储在手机的 `data/data/package_name/shared_prefs/ 目录下生成一个 xml 文件存储数据。

SharedPreferences.Editor 的 commit() 和 apply() 方法区别,如果写入失败了会怎样?

  • commit() 方法是同步的,并发调用时会阻塞。方法会返回一个 boolean 值,新的 value 写入成功后返回 ture,失败返回 false。
  • apply() 方法是异步的,没有返回值。如果 commit() 时有正在执行的 apply 方法,则会 block,直到 apply() 执行完成才会执行 commit()。apply() 写入失败不会有任何提示。

Editro 的真正实现类是 SharedPreferences.EditorImpl 类,当调用 Editor#putXXX 方法时,实际上并不会立刻将修改同步到文件中,而是会保存在一个 mModified(HashMap) 中。当调用 commit() 或 apply() 方法时,才会将 mMap 与 mModified 中的值进行合并处理,并写入到磁盘文件中。

commit()
  • 将对 editor 的操作记录(mModified)同步到 mMap 中
  • 通过 enqueueDiskWrite 方法,将数据同步写入磁盘,需要等待写入完成
  • 通知监听(registerOnSharedPreferenceChangeListener)
  • 返回执行结果
apply()
  • 将对 editor 的操作记录同步到 mMap 中
  • 异步将数据写入磁盘(通过一个 HandlerThread)
  • 不需要等待写入完成,直接返回,没有返回值

SharedPreferences 是否可以跨进程使用?

可以。为 SharedPreferences 设置 MODE_MULTI_PROCESS Flag 就可以,用于一个 App 有多个进程,又想往同一个 Sp 中写入数据时,但是目前官方已经标记为废弃,不建议使用了。

SharedPreferences#getXXX() 方法

  • getXXX 方法是线程安全的,加了 synchronized 关键字
  • getXXX 方法是直接操作内存的,直接从 mMap 中根据传入的 key 读取 value
  • getXXX 方法有可能阻塞,卡在 awaitLoadedLocked 方法。第一次调用 getSharedPreferences 方法时,会创建一个线程去异步加载数据,当数据未加载完时调用 getXXX 方法,此时 mLoaded 为 false,所以会导致其调用 wait() 等待。需要等待数据加载完调用 notifyAll() 来唤醒继续执行

SharedPreferences#putXXX() 方法

  • putXXX 方法需要先通过 sp 获取到一个 Editor 对象,实现类是 EditorImpl
  • 不会直接对 mMap 做操作,对键值对的修改记录保存在一个 Map 中,在 commit/apply 时同步到内存以及磁盘数据中。

SharedPreferences 使用注意事项

  • 第一次构建 SharedPreferences 的时候开启了一个子线程从磁盘获取数据(后面走缓存),不会阻塞 SharedPreferences 的构建。但是会阻塞 getXX/putXX/remove/clear 等调用。
  • 不要使用 sp 在多进程场景使用。没有跨进程的锁,有可能导致数据丢失错乱。
  • 每个 sp 文件不能过大。sp 的文件存储性能与文件大小相关,不要将毫无关联的配置保存在同一个文件中,同时考虑将频繁修改的条目单独隔离出来
  • 还是每个 sp 文件不能过大。第一次调用 getSharedPreferences() 方法时,会先加载 sp 文件进内存,过大的文件会导致阻塞甚至 ANR
  • 每个 sp 文件不能过大。每次 apply 或 commit,都会把全部的数据一次性写入磁盘,sp 文件过大会影响性能。
  • apply 虽然是异步写,但会将异步任务放在 QueuedWork 中,在 Activity、Service 等组件结束时,会遍历 QueuedWork 中的任务进行执行,如果 sp 文件过大,会导致 Activity、Service 生命周期阻塞
    • ActivityThread#handlePauseActivity -> QueuedWork.waitToFinish()

数据库存储

SQLite 是一个轻量级关系型数据库,运算快,占用空间小。

SQLiteOpenHelper 类

是 SQLiteDatabase 的帮助类,用于管理数据库的创建和升级,是抽象类,有两个重要方法,onCreate 和 onUpgrade,用于创建和升级数据库

SQLiteDatabase 类

通过 SQLiteOpenHelper#getReadableDatabase 或者 getWriteableDatabase 可以获取一个 SQLiteDatabase 对象,然后操纵它完成数据的增删改查。

  • SQLiteOpenHelper 有两个回调方法,onCreate 和 onUpgrade,构造函数中有一个参数是 version,数据库版本号,构建 SQLiteOpenHelper 时,如果传递的版本号大于之前的版本号,会自动回调 onUpgrade 方法,在这里完成数据库的升级操作。触发此方法,会将数据库老版本和新版本都作为参数传递过来
  • Android 使用 getWritableDatabase 和 getReadableDatabase 都可以获取一个用于操纵数据库的 SQLiteDatabase 实例
  • getWriteableDatabase 是以读写方式打开,数据库满了,数据库只能读不能写,此时会报错
  • getReadableDatabase 是先以读写方式打开数据库,如果磁盘满了,会打开失败,继续以只读方式尝试打开数据库,如果该问题成功解决,只读数据库对象关闭,返回一个可读写的数据库对象

ContentProvider 读取数据

  • ContentProvider 忽略了数据底层实现,支持从 xml、数据库、文件等资源的读取,通过一系列的 Uri 来操纵数据
  • 声明、注册 ContentProvider
  • 其他 App 通过 ContentResolver 匹配对应 Uri 来读取共享数据的 app 的数据

动画

ObjectAnimator

原理是通过 TypeEvaluator,计算对应动画完成度时,属性的具体值是什么。通过不停改变属性的值,然后调用 invalidate() 方法使 View 无效,等待下一帧 View 刷新到来时刷新 View,来达到动画的效果。可以对任意 Object 对象做属性动画。

使用

val animator = ObjectAnimator#ofXXX()
animator.start()

使用 ObjectAnimator 的要求

  • 要添加动画效果的对象的属性必须有 set() 形式的 setter 函数和 get() 形式的函数(采用驼峰式大小写)。
  • 如果是自定义的属性,则还要在其 setter 函数中手动调用 invalidate() 函数,使 View 失效,以便在下次刷新的时候使用最新值更新 View

invalidate() 方法是把 View 标记为失效,下一帧到来的时候会刷新 View。

PropertyValuesHolder#setupStartValue(target);

设置监听

animator.addUpdateListener(object :ValueAnimator.AnimatorUpdateListener{
      override fun onAnimationUpdate(animation: ValueAnimator?) {
        
      }
})

AnimatorSet

管理多个 ObjectAnimator,可以让其按照某个顺序执行

val animatorSet = AnimatorSet()
// animatorSet.playSequentially(animator1, animator2)  按顺序执行
// animatorSet.playTogether(animator1, animator2)  一起执行
animatorSet.start()

Interpolator

插值器,用于设置时间完成度到动画完成度的计算公式。就是设置动画的速度曲线,比如,LinearInterpolatro(匀速插值器)、AccelerateInterpolator(加速插值器)、AccelerateDecelerateInterpolator(先加速后减速插值器,默认的)等。

TypeEvaluator

设置动画完成度到属性具体值的转换计算。比如,动画完成了百分之二十时,具体的属性值应该是多少。

可以通过仿照 FloatTypeEvaluator 来写出自定义的 TypeEvaluator,完成动画完成度到属性 value 值的映射关系。

硬件加速

  • API14 之后默认开启
  • 可以配合 ViewPropertyAnimator 使用,提高绘制速度,只有 ViewPropertyAnimator 中提供的属性支持赢家加速
view
    .animate()
    .translationX(200f)
    .withLayer()  // 开启硬件加速
  • 开启硬件加速会使用 GPU 绘制,提高绘制效率,因为硬件加速会记录绘制指令,对绘制做优化。
  • 硬件加速有兼容性问题,有的绘制不支持硬件加速

离屏缓冲

View#setLayerType()

针对整个 View,不能针对 onDraw() 里某个过程

  • setLayerType(LAYER_TYPE_HARDWARE):开启离屏缓冲,并使用硬件绘制
  • setLayerType(LAYER_TYPE_SOFTWARE):开启离屏缓冲,并使用软件绘制,一定意义上算是可以关闭硬件加速,因为 Google 没有显示的提供一个关闭硬件加速的方法
  • setLayerType(LAYER_TYPE_NONE):不使用离屏缓冲

Canvas#saveLayer()

针对 Canvas,针对某一绘制过程,所以可以在使用时用 saveLayer 包住需要离屏缓冲的代码

Toast 原理

Android 中的所有 View 都是依附于 Window 存在的,Activity、Dialog、Toast、PopupWindow、Menu 等

  • Toast 的显示隐藏其实是一个 Binder 过程
  • Toast.makeText() 时会创建 Toast 对象和一个 TN 对象,TN 对象是负责与 NMS(NotificationManagerService 这个 Binder 线程交互的对象)
  • 调用 Toast show 方法时,会取 system_server 中的 NMS,调用 NMS 的 enqueueToast 方法。同时将 TN 对象作为 Binder 交互的 callback 传递进去
  • NMS 中根据包名维护了一个 mToastQueue,本质是一个 ArrayList,ArrayList 中存的是 ToastRecord 对象。
  • enqueueToast 方法中会判断同一个 pkg 下 toast 的数量,超出 25 就不会弹出 toast
  • 在 enqueueToast 中构建一个 ToastRecord 对象,将传入的 TN 对象赋值给 ToastRecord 的 callback 属性。存到 mToastQueue 中。调用 showNextToastLocked 方法。
  • showNextToastLocked 会取 mToastQueue 第 0 个元素,调用 ToastRecord 对象的 callback 字段的 show 方法。
  • 回到 Toast 的内部类 TN 的 show 方法中。因为 Toast 的显示隐藏是个 Binder 过程,在 Binder 线程中执行。所以需要通过 Handler 将 Toast 消息发送回创建 Toast 的线程。在 mHandler 中处理 Toast 的消息。
  • handleShow 就是 show toast 的流程,调用 WindowManager 的 addView

适配

屏幕适配

Android 中的 dp 在渲染前会将 dp 转为 px,计算公式为:

  • density = dpi / 160
  • px = density * dp
  • px = dp * (dpi / 160)
    dpi 是根据屏幕真实分辨率和尺寸来计算的,每个设备都可能不一样(使用系统 Api 可以直接获取到)
屏幕尺寸、分辨率、像素密度三者关系

通常情况下,一部手机的分辨率是宽x高,屏幕大小是以寸为单位,那么三者的关系是:

屏幕分辨率为:1920*1080,屏幕尺寸为5吋的话,那么dpi为440。

使用第三方适配库

AndroidAutoSize

宽高限定符适配

穷举可能用到的 Android 设备的宽高像素值,创建对应的 values-AxB 文件夹,如:values-480x320。设定一个基准分辨率,其他分辨率通过这个基准分辨率来计算。在不同尺寸文件夹内创建不同的 dimens 文件,根据定义好的基准分辨率计算各个文件夹中的值。

比如以480x320为基准分辨率

  • 宽度为320,将任何分辨率的宽度整分为320份,取值为x1-x320
  • 高度为480,将任何分辨率的高度整分为480份,取值为y1-y480

那么对于800*480的分辨率的dimens文件来说,

x1=(480/320)*1=1.5px

x2=(480/320)*2=3px

APP运行在不同分辨率的手机中时,这些系统会根据这些dimens引用去该分辨率的文件夹下面寻找对应的值。

问题:这个方案有个问题,需要精确命中才能适配,比如 1920x1080 的手机必须要找到 1920x1080 的限定符,不然就只能用统一的默认的 dimens 文件,使用默认尺寸,UI 又很可能变形,容错机制差。

smallestWidth 限定符适配适配

原理

也叫 sw 限定符适配。指的是 Android 会识别屏幕可用宽高的最小尺寸(其实就是宽),然后根据识别到的结果去资源文件中寻找对应限定符的文件夹下的资源文件。

比如,某手机的 dpi 是 480,横向像素是 1080px,根据 px = dp(dpi/160),可以算出横向的 dp 值是 1080/(480/160),也就是 360dp,系统会去寻找是否存在 value-sw360dp 的文件以及对应的资源文件。

与宽高限定符方案一样,都是系统通过特定的规则来选择对应的文件。但是 sw 限定符适配有很好的的容错性,如果没有 value-swdp 文件夹,系统会向下寻找,找到一个离目标值最近的文件夹,比如没有 value-360dp,则会向下寻找,比如找到了 value-350dp,则用这个文件夹下边的文件。

使用

假设 UI 给定的标准是横向 360dp,我们设备的宽为 375dp,则把 360dp 等分为 375 份,则在该设备上,1dp = 0.96dp。

缺点

多个 dimens 文件可能导致 apk 变大问题。

跨进程通信

为何需要 IPC?

  • 某些模块可能因为一些特殊的原因需要运行在单独的进程,比如单独放在一个进程的下载模块、图片加载模块等。
  • 多个应用之间共享数据
  • 为了加大一个应用可使用的内存,所以需要通过多进程来获取多份内存控件。Android 中对单个应用的最大内存做了限制,一般都以主进程进行限制,而多进程可以打破这个限制。

直接进行跨进程通信可能出现的问题?

Android 为每个应用分配了一个虚拟机,或者说为每个进程分配了一个虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这会导致在不同的虚拟机中访问同一个类的对象会产生多份副本。一般会造成如下一些问题:

  • 静态成员和单例模式完全失效:每个进程独立虚拟机造成的
  • 线程同步机制完全失效:每个进程独立虚拟机造成的
  • Sp 的可靠性下降,这是因为 Sp 不支持两个进程并发进行读写(系统会对 sp 的读写有一定的缓存策略),有一定几率导致数据丢失
  • Application 会被创建多次:Android 系统在创建新的进程时,会分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程,相当于系统又把这个进程重新启动了一遍。

可以这样理解多进程:相当于两个不同的应用采用了 SharedUID 的模式。

AIDL

关键类和方法

  • AIDL 接口,继承 IInterface
  • Stub 类:Binder 的实现类,服务端通过这个类来提供服务
  • Proxy 类:服务端的本地代理,客户端通过这个类调用服务端方法(本质上是一个 IBinder 对象,在 Service 的 onBind 方法中返回)
  • asInterface():客户端调用,将服务端返回的 IBinder 对象转换成客户端需要的 AIDL 接口类型的对象。如果客户端和服务端位于同一进程,则直接返回 Stub 对象本身,否则返回系统封装后的 Stub.proxy 对象(asInterface 方法中会判断要返回哪个对象)
  • asBinder():返回代理 Proxy 的 Binder 对象
  • onTransact():运行在服务端的 Binder 线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交给此方法处理。
  • transact():运行在客户端,客户端发起远程请求的同时将当前线程挂起,之后调用服务端的 onTransact() 方法直到远程请求返回,当前线程才继续执行。

语法

AIDL 的语法基本与 Java 保持一致,有以下几点规则:

  1. AIDL 文件以 .aidl 为后缀名
  2. AIDL 支持的数据类型如下
    • 八种基本数据类型:int、short、long、float、double、boolean、char、byte
    • String、CharSequence
    • 实现了 Parcelable 接口的数据类型
    • List 类型。List 承载的数据必须是 AIDL 支持的类型,或者是其它声明的 AIDL 对象
    • Map 类型。Map 承载的数据必须是 AIDL 支持的类型,或者是其它声明的 AIDL 对象
  3. AIDL 文件分两类
    • 一类用来声明实现了 Parcelable 接口的数据类型,以供其它 AIDL 文件使用那些非默认支持的数据类型(扩展的实现了 Parcelable 的 Javabean 对象,如下边的 Book.aidl 文件)
    • 一类用来定义接口方法,声明要暴露哪些接口给客户端调用,定向 Tag 就是用来标注这些方法的参数值
    • 定向 Tag
  4. 明确导包。在 AIDL 中声明的接口文件,需要手动指定 package,并明确标明引用到的数据类型所在的包名,即使两个文件处在通个包名下。

使用

  • 在 main 目录下,java 同级创建 aidl 文件夹
  • 在 aidl 文件夹下,创建应用同包名目录结构,然后在其中添加 aidl 文件
  • 如要引用 java 包下的类,如 Javabean 等,需要手动在 aidl 文件中 import 引用,同时 aidl 文件需要有 package。
// IBookManager.aidl
package com.example.interviewdemo; // 这个是必须的,否则编译时 aidl 生成 java 文件会找不到 Book 引用
import com.example.interviewdemo.Book;  // 这个也是必须的

interface IBookManager {
    List<Book> getBookList();

    void addBook(in Book book);
}

// Book.aidl
// Declare Book so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Book;


// Book.java  定义在 java 目录下的工程中的 JavaBean 
public class Book implements Parcelable {

    public int bookId;
    public String bookName;


    protected Book(Parcel in) {
        bookId = in.readInt();
        bookName = in.readString();
    }

    public static final Creator<Book> CREATOR = new Creator<Book>() {
        @Override
        public Book createFromParcel(Parcel in) {
            return new Book(in);
        }

        @Override
        public Book[] newArray(int size) {
            return new Book[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(bookId);
        dest.writeString(bookName);
    }
}

使用 CopyOnWriteArrayList

AIDL 方法是在服务端 Binder 线程池中执行的,当多个客户端同时连接的时候,会存在多个线程同时访问的情况,处理 List 的线程同步,可以使用 CopyOnWriteArrayList 进行 List 线程同步的管理。

AIDL 中所支持的是抽象的 List,List 是一个接口,因此虽然服务端返回的是 CopyOnWriteArrayList,但是在 Binder 中会按照 List 的规范去访问数据,最终生成一个 ArrayList 传递给客户端。类似的还有 ConcurrentHashMap。

使用 AIDL 要注意的

  • 客户端调用远程服务的方法,被调用的方法运行在服务端 Binder 线程池中,方法调用时,客户端线程会被挂起,要注意如果是在 UI 线程,不要调用服务端耗时方法。
  • 客户端的 onServiceConnected 和 onServiceDisconnected 方法都运行在 UI 线程,所以也不可以在这里调用服务端耗时方法。
  • 服务端调用客户端的 listener 中的方法时,被调用的方法运行在客户端的 Binder 线程池中。所以同样不可以在服务端中调用客户端的耗时方法,否则可能导致服务端无响应。
  • 服务端访问客户端的 listener 中的方法时,客户端的具体回调此时是在客户端 Binder 线程池中的,所以不能在这里访问 UI 相关的操作。

AIDL 如何让服务端调用客户端的方法?

  • 定义一个 AIDL 接口,向服务端注册这个接口的回调,并使用 RemoteCallbackList 管理。
  • 在客户端实现这个接口,同时因为服务端调用接口的方法是在 Binder 线程池中执行,还需要一个 Handler 将消息发送到主线程中执行(类似 AMS 与 ActivityThread#ApplicationThread 中的交互,通过 H 这个 Handler)。

RemoteCallbackList 的作用

RemoteCallbackList 是系统专门提供的用于删除跨进程 listener 的接口。支持管理任意的 AIDL 接口,因为所有的 AIDL 接口都是实现了 IInterface 的。

在 Binder 中使用 List 保存 listener 完成注册与反注册是否可行?为什么?

如果使用普通的注册、反注册接口的方式,也就是直接传递接口保存到 List 中的方式,那么反注册时会找不到要反注册的 listener。因为 Binder IPC 的过程中是不能直接传递对象的,对象的跨进程传输本质上都是序列化、反序列化的过程,所以 AIDL 中的对象都要实现 Parcelable 接口。Binder 会把客户端传递过来的对象重新转化并生成一个新的对象。

RemoteCallbackList 的工作原理

在多次跨进程传输中,客户端的同一个对象虽然会在服务端生成不同的对象,但是这些新生成的对象的 Binder 对象是同一个,RemoteCallbackList 就是利用了这一特性。当客户端接注册时,遍历服务端所有 listener,找到和解注册的 listener 具有相同的 Binder 对象的服务端 listener 把它删除即可。这就是 RemoteCallbackList 做的事。

  • RemoteCallbackList 内部维护了一个 Map 结构专门用来保存所有的 AIDL 回调,这个 Map 的 key 是 iBinder 类型,value 是 Callback 类型
ArrayMap<IBinder,Callback> mCallbacks = new ArrayMap<IBinder,Callback>();
  • Callback 中封装了真正的远程 listener。当客户端注册 listener 的时候,它会把这个 listener 信息存入 mCallbacks 中,key 和 value 的获得方式如下:
IBinder key = listener.asBinder();
Callback value = new Callback(listener,cookie);
  • 使用时先调用 int n = RemoteCallbackList#beginBroadcast(),获取元素个数
  • 通过遍历 n,调用 RemoteCallbackList#getBroadcastItem(i) 获取某个 listener 对象,然后可以调用 listener 对象中的方法
  • 最后需要调用 RemoteCallbackList#finishBroadcast 方法清理 broadcast 的状态
RemoteCallbackList 的特性
  • 客户端进程终止后,它能自动移除客户端注册的 listener
  • RemoteCallbackList 内部自动实现了线程同步的功能,使用时不需要做额外的线程同步工作

AIDL Binder 意外死亡如何处理

当服务进程意外停止时,需要重新连接服务。

  • 给 Binder 设置 DeathRecipient 监听,当 Binder 死亡时,会收到 binderDied 方法的回调,在 binderDied 方法中可以重连远程服务器。(在客户端 Binder 线程池中被回调)
  • 在 onServiceConnectedDisconnected 中重连远程服务。(在客户端 UI 线程被回调)

在 AIDL 中使用权限验证

默认情况下,远程服务任何人都可以连接,可以加入权限验证功能,权限验证失败则无法调用服务器中的方法。

  • 在 onBind 中进行验证,不通过直接返回 null。
    • 在 AndroidManifest 中声明权限,在 onBind 方法中做权限验证,若 App 无这个权限,则直接返回 null,绑定也就失败了。
  • 在服务端的 onTransact 方法中进行权限验证,验证失败直接返回 false。
    • 可以采用 permission 验证
    • 可以采用 Uid 和 Pid 验证。通过 Binder#getCallingUid 和 getCallingPid 可以拿到客户端所属的应用的 Uid 和 Pid,通过这个参数可以做一些验证工作,比如包名。

其他

Android 中的 Context

Context 是抽象类,继承关系如下所示:

APK 打包流程

pic

  1. 通过 AATP 工具将资源文件和 AndroidManifest 等文件打包生成 R.java 文件
  2. AIDL 工具将 aidl 文件生成对应的 java 文件
  3. javac 工具将 .java 文件生成 .class 文件
  4. d8 工具将 .class 文件打包生成 .dex 文件
  5. apkbuilder 将资源文件、dex 文件打包成一个未签名的 Apk
  6. jarsigner 工具使用签名文件对未签名的 Apk 进行签名
  7. zipalign 对签名后的 Apk 进行对齐处理。对齐的过程就是将 Apk 文件中所有的资源文件的起始距离都偏移 4 字节的整数倍,这样通过内存映射访问 Apk 文件的速度会更快。

APK 的安装流程

pic

  1. 复制 Apk 到 /data/app 目录下,解压并扫描安装包
  2. 资源管理器解析 Apk 里的资源文件
  3. 解析 AndroidManifest 文件,并在 /data/data 目录下创建对应的应用数据目录
  4. 对 dex 文件进行优化,并保存在 dalvik-cache 目录下
  5. 将 AndroidManifest 文件解析出的四大组件信息注册到 PackageManagerService 中。
  6. 安装完成后,发送广播

APT(Annotation Processing Tool)

Annotation Processing Tool 注解处理器,是一种注解处理工具,用来在编译期扫描和处理注解,通过注解生成 Java 文件。以注解作为桥梁,通过预先定好的代码生成规则来自动生成 Java 文件。ButterKnife、Dragger2 中都使用了 APT。

Java API 已经提供了扫描源码并解析注解的框架,继承 AbstractProcessor 类实现自己的注解解析逻辑。APT 的原理就是在注解了某些代码元素(字段、函数、类)后,在编译时编译器会检查 AbstractProcessor 的子类,并自动调用 process() 方法,然后将添加了指定注解的所有代码元素作为参数传递给该方法,开发者根据注解元素在编译期输出对应的 Java 代码。

  • 创建一个工程(Java or Kotlin Library)
  • 在主项目的 build.gradle 中使用 annotationProcessor 引入
  • 创建一个类 BindingProcessor,继承 AbstractProcessor
  • 在 main 包下创建 resources/META_INF/services 文件夹
  • 在该文件夹下创建文件 javax.annotation.processing.Processor
  • 在上述文件中引入 BindingProcessor 的引用
  • 重写 BindingProcessor 中的 process() 方法和 getSupportedAnnotationTypes() 方法
    • getSupportedAnnotationTypes() 方法返回要对哪些注解进行处理,是一个 Set 集合
  • 在 process() 方法中使用 Javapoet 生成指定格式的代码,最后通过 JavaFile 写入文件

如何处理全局异常捕获(CrashHandler)

当线程由于未捕获的异常即将终止时,JVM 会使用 Thread#getUncaughtExceptionHandler 方法查询线程的 UncaughtExceptionHandler,并调用它的 uncaughtException(Thread) 方法,开发者可以在这个方法中捕获异常,打印 log,写入文件等操作。

使用

  • 定义 CrashHandler 类,实现 Thread.UncaughtExceptionHandler 接口
  • 重写 uncaughtException 方法,可以在里边做打印异常信息、写日志等操作
  • 在 Application 中实例化,调用 Thread.setDefaultUncaughtExceptionHandler(handler); 方法将异常处理对象传入进去(可以将 CrashHandler 定义为单例,提供一个 init 方法,在其中设置 setDefaultUncaughtExceptionHandler)
public class CrashHandler implements Thread.UncaughtExceptionHandler {
    private Thread.UncaughtExceptionHandler mDefaultCrashHandler;

    public void init() {
        // 获取系统默认的 UncaughtException 处理器
        mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler();
        // 设置该 CrashHandler 为程序的默认处理器
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("enter uncaughtException:" + e);
          System.out.println("Crash Thread name:" + t.getName() + " Crash Thread id:" + t.getId());
          // 1. 保存文件到本地......
          // 2. 上传文件到服务器等操作
        // 如果系统提供了默认的异常处理器,则交给系统去结束程序,否则处理自己的逻辑
        if (mDefaultCrashHandler != null) {
            mDefaultCrashHandler.uncaughtException(t, e);
        } else {
          // 按照业务需求定义处理操作,可以杀死自己
          Process.killProcess(Process.myPid());
        }
    }
}

// 在 Application 中使用
public class App extends Application{
    @Override
    public void onCreate(){
        new CrashHandler().init();
    }
}

注意

  • 因为 Thread 中的 defaultUncaughtExceptionHandler 是个静态成员变量,所以它的作用对象是当前进程的所有线程
  • 代码中被 catch 的异常不会交给 CrashHandler 处理,CrashHandler 只能收到那些未捕获的异常

内存泄露

Android 中的内存泄露,一般是长生命周期的对象持有了短生命周期的对象,导致短生命周期对象无法释放

  • 单例导致的内存泄露:单例对象的生命周期等同于 App,如果一个对象没有用了,但是单例还持有它的引用,则这个对象不能被正常回收,就泄露了,比如单例持有 Activity 的 Context
  • 非静态内部类:Java 中的匿名内部类隐式的持有外部类的强引用,如果在 Activity 中声明,则会持有 Activity 的引用,可能导致 Activity 无法正常销毁,如 Handler、Thread 等。一般这种匿名内部类常见于监听器。
  • 资源使用后未关闭:如 BroadcastReceiver、ContentProvider、File、Cursor、Stream、Bitmap 等。这些资源在读写操作时通常都使用了缓冲,如果不及时关闭,这些缓冲对象会一直占用内存得不到释放,导致内存泄露
  • WebView 造成的内存泄露:WebView 在加载网页后会长期占用内存而不能被释放,需要在 Activity 销毁时,将其从父容器移除,然后调用它的 destroy 方法销毁它以释放内存
// 先从父控件中移除WebView
    mWebViewContainer.removeView(mWebView);
    mWebView.stopLoading();
    mWebView.getSettings().setJavaScriptEnabled(false);
    mWebView.clearHistory();
    mWebView.removeAllViews();
    mWebView.destroy();
  • 属性动画造成内存泄露:比如在 Activity 中开启了属性动画,但是 Activity 销毁时没有调用 cancel 方法。动画引用了 View,View 引用了 Activity,所以此时会导致 Activity 无法释放
  • 集合类内存泄露:如果将一个对象放入 ArrayList、HashMap 中,这个集合就会持有该对象的引用。当我们不需要这个对象时,如果没有从集合移除,只要集合还在使用,这个对象就造成了内存泄漏。如果一个集合类是静态的,集合里没有使用的对象更会造成内存泄露了。应该及时将不需要使用的对象从集合 remove,或者 clear 集合。
  • 未取消注册或回调导致的内存泄露:比如 BroadcastReceiver、或者是 MVP 中 P 层对 V 层的引用
  • Timer 和 TimerTask:如果 TimerTask 中持有了 Activity 的 Context,如果 Activity 销毁时,没有 cancel 掉 Timer 和 TimerTask,Timer 还在继续等待执行 TimerTask,那么就会导致泄露

查找内存泄露可以使用 AS 中自带的 Android Profiler 工具或引入 LeakCanary 库。

Context 相关

Android 应用里有几种 Context 对象?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IOkiXxeN-1622169847533)(https://github.com/guoxiaoxing/android-open-source-project-analysis/raw/master/art/app/component/context_uml.png)]
Activity、Service、Application 都间接的继承自 Context 类

  • Dialog 中不能使用 Application context 创建 Dialog,验证 token 会失败抛出异常,Dialog 只支持以 Activity 的 token 创建。
  • Activity 和 Service 以及 Application 的 Context 不一样,Activity 继承自 ContextThemWrapper,其他的继承自 ContextWrapper
  • Activity 和 Service 中使用 getApplication 获取 Application,BroadcastReceiver 中使用 getApplicationContext 方法
  • Context 的数量等于 Activity 的数量 + Service 的数量 + 1(Application)

Android 中进程的优先级?

  1. 前台进程
    用户正在交互的 Activity 或 Activity 用到的 Service 等,系统内存不足时,前台进程是最晚被杀死的
  2. 可见进程
    可以是处于暂停状态(onPause)的 Activity 或绑定在其上的 Service,即被用户可见,但由于失去焦点不能与用户交互
  3. 服务进程
    正在运行的,使用 startService() 方法启动的服务,且不属于上述两个更高类别进程的进程。比如,在后台播放音乐或下载数据。
  4. 后台进程
    包含目前对用户不可见的 Activity 进程(已经调用 onStop() 方法)。这些进城一般对用户体验没有直接影响,内存不足时会首先回收。
  5. 空进程
    不包含任何应用程序的进程,这样的进程一般系统不会让他存在

多进程场景遇到过吗?

  1. 在新的进程中,启动前台 Service,播放音乐
  2. 多进程开发可以一定程度上避免 OOM,因为 Android 对内存的限制是针对主进程的。比如当我们要加载大图的时候可以去新的进程执行,避免主进程 OOM,假如图片浏览进程崩溃,不会影响主进程。

Android 类加载器

Android 平台上虚拟机运行的是 Dex 字节码,是一种对 class 文件优化后的产物。传统 Class 文件是一个 Java 源码文件对应一个 .class 文件,Android 中是把所有 Class 文件进行合并,优化,最终生成一个 class.dex 文件,目的是把不同 class 文件中重复的东西只保留一份。如果 Android 应用不进行分 dex 处理,最后一个应用的 apk 只会有一个 .dex 文件。

Android 中常用的类加载器有两个,DexClassLoader 和 PathClassLoader,他们都继承于 BaseDexClassLoader。区别在于调用父构造器时,DexClassLoader 可以多传入一个 optimizedDerectory 参数,这个目录必须是内部存储路径,用来缓存系统创建的 Dex 文件。PathClassLoader 该参数为 null,只能加载已经安装到 Android 系统的 APK 文件。DexClassLoader 可以加载任意目录下的 dex、jar、apk、zip 文件。不过这个参数在 API26 以上废弃了。Android 中默认的类加载器是 PathClassLoader。

子线程可以弹 Toast 和 Dialog 吗?正确姿势是?

不可以。本质上 Toast 的实现依赖其中的一个静态内部类 TN,TN 中创建了一个 Handler 处理 Toast 消息。也就是 Toast 实际上是依赖于 Handler 的。我们调用 Toast.makeText().xx 方法时,内部会调用 Toast 的构造函数,在构造函数中会实例化 TN,传入了一个 null 的 Looper,在 TN 的构造函数中会创建一个 Looper 对象并新建一个 Handler。但是并没有调用 Looper.loop() 方法。所以子线程弹 Toast 会报错。

Dialog 内部也是基于 Handler 发送消息,子线程如果未调用 Looper.prepare() 和 Looper.loop() 开启循环,也会报错。

  • 可以将 Toast 的弹出 post 到主线程
  • 在子线程开启 loop 循环之后再 toast

MultiDex

使用 MultiDex 解决何事?根本原因在于?

Dalvik Executable 规范将可在单个 DEX 文件内引用的方法总数限制为 65536(底层使用了无符号的 short 数组),其中包括 Android 框架方法、库方法以及自己的代码中的方法。
使用 MultiDex 主要解决方法数 65535 限制的问题,即方法数不能超过 65535 个。

主 Dex 文件放那些东西,跟其他 Dex 调用、关联?

主 Dex 文件存放应用启动就必须加载的类,可以使用 multiDexKeepFile/multiDexKeepProguard 属性声明这些类,手动将其指定为 Dex 文件中的必须类。

Odex 的作用?

Odex 的作用主要在预处理,可以缩短较长的增量构建时间,Odex 可以在构建之间重用 MultiDex 输出。但只在 Android 5.0(API21)以上支持(ART)。

规避 64K 限制

官方文档

  • minSdkVersion 为 21 或更高版本,系统会默认启用 MultiDex,并且不需要 MultiDex 库
  • minSdkVersion 为 20 或更低版本,必须配置 MultiDex 库并对项目做修改
    • 修改 module 级 build.gradle 文件以启用 MultiDex,并将 MultiDex 库添加到依赖项
        android {
            defaultConfig {
                ...
                minSdkVersion 15
                targetSdkVersion 28
                multiDexEnabled true
            }
        ...
        }
    
        dependencies {
            implementation "androidx.multidex:multidex:2.0.1"
        }
    
    • 若不替换 Application 类,在 Manifest 文件中配置:
    • 如果替换 Application,继承 MultiDexApplication
    • 或者继承自己定义的 Application,但在 Application 的 attachBaseContext 方法中配置 MultiDex.install(this) 方法

注意:在 MultiDex.install() 完成之前,不要通过反射或 JNI 执行 MultiDex.install() 或其他任何代码。MultiDex 的多个 dex 文件之间的跟踪功能不会追踪这些调用,从而导致出现 ClassNotFoundException,或因 DEX 文件之间的类分区错误而导致验证错误。

组件化、插件化

组件化

组件化、模块化是类似的,拆分成多个 module 开发就是组件化。

插件化

App 的部分功能模块在打包时不以传统方式打包进 apk 文件中,可以放在网络上适时下载,在需要的时候动态对这些功能模块进行加载,就是插件化。

这些单独二次封装的功能模块 apk 就是插件。

插件化原理

插件化的原理是动态加载,通过自定义 ClassLoader 来加载新的 dex 文件(这个 ClassLoader 持有这个插件的所有 .class 文件),从而让程序原本没有的类可以被使用,就是插件化的原理。

  • 构建一个 DexClassLoader/PathClassLoader
  • 使用构建出来的 ClassLoader 对象加载外部 apk/dex 文件(PathClassLoader 只能访问 app 包下的内容,DexClassLoader 可以访问任意路径下的 apk 文件)
  • 通过反射去实例化插件中的类,然后调用其中的方法。

在宿主 App 不能加载插件中的 Activity,因为在宿主 App 的 AndroidManifest 中没有声明,可以通过 Fragment/View 的形式替代

DexClassLoader classLoader = new DexClassLoader(apk.getPath(), getCacheDir().getPath(), null, null);
    try {
      Class utilsClass = classLoader.loadClass("com.hencoder.plugin.Utils");
      Constructor utilsConstructor = utilsClass.getDeclaredConstructors()[0];
      utilsConstructor.setAccessible(true);
      Object utils = utilsConstructor.newInstance();
      Method shoutMethod = utilsClass.getDeclaredMethod("shout");
      shoutMethod.setAccessible(true);
      shoutMethod.invoke(utils); // 调用 utils 对象的 shoutMethod
      Intent intent = new Intent();
      intent.setClassName("com.hencoder.plugin", "com.hencoder.plugin.MainActivity");
      startActivity(intent);
    } catch (...) {
        ...
    }
插件化的作用
  • 早期用来解决 dex 65535 问题
  • 可以减小安装包大小
  • 实现动态部署,模块化发布
  • bug 热修复:在特定位置预留好功能点入口,点击时,加载对应插件
常用的插件化框架
  • 滴滴的 VirtualAPK
  • 360 的 RePlugin

热修复

常用于 bug 修复,或者是对软件进行局部更新。

热修复与插件化的区别

  • 插件化的内容在原 App 中没有,而热更新是原 App 中的内容做了改动
  • 插件化在代码中有固定的入口,而热更新则可能改变任何一个位置的代码

热修复的原理

  • ClassLoader 的 dex 文件的替换
    • 利用 loadClass 类加载过程
  • 直接修改字节码

通过干预 ClassLoader findClass 的过程实现热修复

loadClass() 的类加载过程(Android 中类加载机制)

Android 中的类加载机制,也即是网上常说的双亲委托机制,其实就是一个带缓存的自上而下的加载过程。对于一个 ClassLoader 而言,加载一个类需要调用其 loadClass() 方法,loadClass 方法中,会做如下几件事:

  1. 会先去自己的缓存中找是否有 class
  2. 缓存中没有,则判断是否有父 ClassLoader,若有父 ClassLoader,则向父 ClassLoader 要,是同样的类加载过程
  3. 父 ClassLoader 中也没有,则尝试调用自己的 findClass 方法,加载并缓存,且只有自己加载的 class 才会进行缓存

也即,若父 ClassLoader 中有要寻找的 class,则即便子 ClassLoader 有能力加载,也不会重复加载。

Android 中默认的 ClassLoader 是 PathClassLoader,PathClassLoader 的父类是 BaseDexClassLoader。

类的加载机制没办法改变,所以 ClassLoader 这种方案的热更新就是干预类自己的加载过程(应用类加载器)。即 BaseDexClassLoader 的 findClass() 中的逻辑

通过 ClassLoader 方式热修复的具体流程
  • BaseDexClassLoader 的 findClass 中,会调用 DexPathList 的 findClass(className) 方法
  • DexPathList#findClass() 方法中会遍历 Element 数组 dexElements,调用它的 findClass() 方法寻找 class
  • Element 数组是 DexPathList 实例化时,根据 path 初始化的,而 DexPathList 的初始化是 BaseDexClassLoader 的构造函数中。即 Element 数组中,存的是 class 文件的路径
  • 所以热更新的关键就是把补丁 dex 文件加载放进一个 Element,并且插入到 DexPathList 的 Element 数组中的最前面(将其他元素后移一位),使其最先加载

热更新加载完成后,需要先杀死程序,清除掉已经加载过的 ClassLoader 的缓存,再次打开 App 才能让补丁生效,因为老的 class 文件已经被加载过了,如果不杀死程序,根据双亲委派原理,则新的补丁永远不会生效

常用的热更新框架

  • 微信的 Tinker
  • 支付宝 AndFix
  • 美团的 Robust 方案

参考文章

Activity 面试黑洞 - 当按下 Home 键再切换回来会发生什么

配置构建

(Android 9.0)Activity启动流程源码分析

为什么 Activity 的 onStop 延迟了 10s 执行

踩坑之路:finish方法执行后居然还有这种操作?

Android进程保活之一个像素保活

Tasks and back stack

Handler 27 问

Handler 的同步屏障

Android IdleHandler 机制

每日一问 听说过Handler中的IdleHandler吗?

今日头条屏幕适配方案

Android 目前稳定高效的UI适配方案

今日头条屏幕适配方案终极版正式发布!

属性动画

ViewGroup 为什么不会调用 onDraw

小知识又来了!ViewGroup onDraw为什么不调用?

直面底层:经常用的ViewTreeObserver 背后的原理

面试高频题:一眼看穿 SharedPreferences

Android Storage example ExternalStorage.java

再谈Android各种Context的前世今生!

ConstraintLayout

MotionLayout samples

MotionLayout Guide

数据绑定库

Android Transition

Android KTX library

面试官:简历上最好不要写Glide,不是问源码那么简单

聊一聊关于Glide在面试中的那些事

直面底层:Window/WindowManager 不可不知之事

直面底层:WindowManager 视图绑定以及体系结构

Android 全局异常处理

使用 Adb shell dumpsys 检测 Android 的 Activity 任务栈

Android 自定义View之Measure过程

Android 自定义View之Layout过程

onMeasure() 为什么会执行多次?

requestLayout 竟然涉及到这么多知识点

Android Resources之assets

Android 混淆、压缩

Proguard 规则文档

优先使用 KTX 库 | MAD Skills

Android常见内存泄漏及优化总结

再次回顾 Android View 核心知识与原理

玩转自定义 View,你必须搞清楚这些:Style,Theme,Attr,Styleable,TypedArray

服务概览

全面复盘Android开发者容易忽视的Backup功能

Android scrollTo、scrollBy、以及scroller详解

Android ContentProvider 初始化过程

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-02 10:55:55  更:2021-08-02 10:56:27 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/20 3:10:37-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码