Android面试
目录
网络协议:
TCP、HTTP、HTTPS这几个被问到的概率极高
HTTP的报文格式
- 请求报文
- 请求行:由请求方法、URL(包含参数)和协议版本组成
- 请求头:由多个key-value值组成
- 空行:请求报文使用空行将请求头部和请求数据分隔
- 请求数据:GET方法没有携带数据,POST方法会携带一个body
- 响应报文
- 状态行:由协议版本、状态码和状态值组成
- 响应头:由多个key-value值组成
- 空行:响应报文使用空行将响应头和响应体分隔
- 响应体:响应数据
HTTP2.0
- 新的二进制格式,HTTP1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮。
- 多路复用,即连接共享,即每一个request都是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面
- header压缩,对前面提到过HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小
- 具有服务端推送功能
WebSocket
基于TCP的应用层协议
- WebSocket是双向通信模式,客户端与服务器之间只有在握手阶段是使用HTTP协议的“请求-响应”模式交互,而一旦连接建立之后的通信则使用双向模式交互,不论是客户端还是服务端都可以随时将数据发送给对方;而HTTP协议则至始至终都采用“请求-响应”模式进行通信。也正因为如此,HTTP协议的通信效率没有WebSocket高。
- 实时数据刷新,早期实现实时数据定时查询,轮询的方式,都会照成带宽的浪费,因为服务器可能没有更新,无效请求
- 相比起HTTP协议,WebSocket具备如下特点
- 支持双向通信,实时性更强
- 较少的控制开销:连接创建后,WebSockete客户端、服务端进行数据交换时,协议控制的数据包头部较小
TCP是怎么确保是可靠传输的
- TCP发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层
- 校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。
- TCP的接收端会丢弃重复的数据。
- 流量控制: TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制)
- 拥塞控制: 当网络拥塞时,减少数据的发送。
- 停止等待协议: 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。 超时重传: 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
TCP3次握手4次挥手
- 握手:
- c->s 请求建立连接
- s->c 做出回应,并请求建立连接
- c->s 对s的请求做出回应
- 挥手:
- c->s 请求断开连接
- s->c 确定并同意断开,但不会立即断开,有可能还有数据需要发送
- s->c s端数据发送完了,请求断开连接
- c->s 对s端进行确认回应
HTTPS原理是怎么样,它怎么保证安全传输
HTTP有以下三个缺点:无加密,无身份认证,无完整性保护,因此所谓的HTTPS,它其实就是HTTP+加密+身份认证+完整性保护
- 客户端向服务器发起SSL通信,报文中包含客户端支持的SSL的指定版本,加密组件列表(所使用的加密算法及密钥长度)
- 服务器的响应报文中,包含SSL版本以及加密组件,服务器的加密组件内容是从客户端发来的加密组件列表中筛选出来的,服务器还会发一个公开密钥并且带有公钥证书
- 客户端拿到服务器的公开密钥,并验证其公钥证书(使用浏览器中已经植入的CA公开密钥)
- 如果验证成功,客户端生成一个Pre-master secret随机密码串,这个随机密码串其实就是之后通信要用的对称密钥,并用服务器的公开密钥进行加密,发送给服务器,以此通知服务器,之后的报文都会通过这个对称密钥来加密
- 同时,客户端用约定好的hash算法计算握手消息,然后用生成的密钥进行加密,一起发送给服务器
- 服务器收到客户端发来的的公开密钥加密的对称密钥,用自己的私钥对其解密拿到对称密钥,再用对称密钥解析握手消息,验证hash值是否与客户端发来的一致。如果一致,则通知客户端SSL握手成功
- 之后的数据交互都是HTTP通信(当然通信会获得SSL保护),且数据都是通过对称密钥来加密(这个密钥不会每次都发,在握手的过程中,服务器已经知道了这个对称密钥,再有数据来时,服务器知道这些数据就是通过对称密钥加密的,于是就直接解密了)
C端发起SSL/TLS通讯(带有ssl版本和加密组件列表)-> S端响应带有公开秘钥和公钥证书 -> C端拿到公开秘钥并验证公钥证书->验证成功后生成一个随机密码串,该串为对称加密,对密码串进行hash,并用S端的公钥加密,一起把hash和密码串发给S端->S端再用私钥把对称密码串解码,并且验证hash是否被篡改,如果无篡改,则ssl握手成功了
RPCX
服务端在多个国家本地部署代理服务器,客户端向本地代理服务器发送请求,然后代理服务器向最终的服务器转发请求并向客户端返回结果。
- 基于DNS服务自动找到本客户端访问最快的代理服务器节点
- 对客户端请求的http包内容进行编码和加密
- 向选中的代理服务器发送请求并得到结果
数据结构
线性表、数组、链表
- 线性表是具有相同类型的n个数据元素的有限序列
- 数组、链表都是线性表结构
- 数组在内存中是连续的内存区域
- 链表是不连续的内存空间
- 数组的优点:随机访问性强,查找速度快(连续内存空间导致的)
- 数组的缺点:
- 从头部删除、从头部插入的效率低,因为需要相应的向前搬移和向后搬移
- 可能浪费内存 内存空间要求高,必须有足够的连续内存空间。数组大小固定,不能动态拓展
- 链表的优点: 插入删除速度快 内存利用率高,不会浪费内存 大小没有固定,拓展很灵活。
- 链表的缺点: 查找效率较低
排序算法
- 冒泡排序: 相邻的两两比较
public class BubbleSort {
public static int[] bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return arr;
}
int n = arr.length;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n -i - 1; j++) {
if (arr[j + 1] < arr[j]) {
int t = arr[j];
arr[j] = arr[j+1];
arr[j+1] = t;
}
}
}
return arr;
}
)
- 快速排序:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。快速排序是选择一个数值大小作为基准来划分数据集,而归并排序是将序列的长度的前后两部分作为划分数据
Java基础
wait sleep
-
java中的sleep()和wait()的区别
- 对于sleep()方法,我们首先要知道该方法是属于Thread类中的。
- 而wait()方法,则是属于Object类中的。
- sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
- 在调用sleep()方法的过程中,线程不会释放对象锁。
- 而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
synchronized和volatile,线程同步
- synchronized
- 互斥同步
- 当某个线程访问被 synchronized 标记的方法或代码块时,这个线程便获得了该对象的锁,其他线暂时无法访问这个方法,只有等待这个方法执行完毕或代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法代码块。
- volatile
- 保证了不同线程对这个变量进行操作时的可见性即一个线程修改了某个变量的值,这新值对其他线程来是立即可见的。 但是像i++这种操作,涉及到两步,取值再修改值,取到的值是对的,但是在自增的时候可能其他线程已经修改了
- 禁止进行指令重排序 (指令重排序是指指令乱序执行,即在条件允许的情况下直接运行当前有能力立即执行的后续指令,避开为获取一条指令所需数据而造成的等待,通过乱序执行的技术提供执行效率)
- ReentrantLock
- AQS
线程利用率
- 使用多线程就一定效率高吗?有时候使用多线程并不是为了提高效率,而是使得CPU 能同时处理多个事件。
- 某种任务,虽然耗时,但是不消耗 CPU 的操作时间,开启个线程,效率会有显著提高。比如读取文件,然后处理。磁盘IO 是个很耗费时间,但是不耗 CPU 计算的工作。所以可以一个线程读取数据,一个线程处理数据。肯定比一个线程读取数据,然后处理效率高。因为两个线程的时候充分利用了 CPU 等待磁盘 IO 的空闲时间。
如何停止一个线程
- 使用标志位,while循环
- Thread.stop() 被弃用了
- 调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
- 调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。
- Thread.interrupt() 中断,线程中断并不会立即终止线程,而是通知目标线程
类加载机制
- 加载过程:加载、验证、准备、解析、初始化、使用、卸载。
- 双亲委派
- 如果一个类加载器收到了一个类加载的请求,它首先不会去加载类,而是去把这个请求委派给父加载器去加载,直到顶层启动类加载器,如果父类加载不了(不在父类加载的搜索范围内),才会自己去加载
- 双亲委派模型的意义在于不同的类之间分别负责所搜索范围内的类的加载工作,这样能保证同一个类在使用中才不会出现不相等的类,举例:如果出现了两个不同的Object,明明是该相等的业务逻辑就会不相等,应用程序也会变得混乱
Java四大引用
- 强引用:代码中普遍存在的,只要强引用还存在,垃圾收集器就不会回收掉被引用的对象。
- 软引用:SoftReference,用来描述还有用但是非必须的对象,当内存不足的时候会回收这类对象。
- 弱引用:WeakReference,用来描述非必须对象,弱引用的对象只能生存到下一次 GC 发生时,当 GC 发生时,无论内存是否足够,都会回收该对象。
- 虚引用:PhantomReference,一个对象是否有虚引用的存在,完全不会对其生存时间产生影响,也无法通过虚引用取得一个对象的引用,它存在的唯一目的是在这个对象被回收时可以收到一个系统通知
垃圾回收
- 引用计数法,有对这个对象的引用就+1,不再引用就-1,但是这种方式看起来简单美好,但它却不能解决循环引用计数的问题。因此可达性分析算法登上历史舞台,用它来判断对象的引用是否存在。
- 可达性分析算法,通过一系列称为 GCRoots 的对象作为起始点,从这些节点从上向下搜索,所走过的路径称为引用链,当一个对象没有任何引用链与GCRoots连接时就说明此对象不可用,也就是对象不可达。
- 回收算法:
- 标记-清除 标记-清除算法采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
- 标记-整理 标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。该垃圾回收算法适用于对象存活率高的场景(老年代)。
- 复制 复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,比如新生代。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。
- 分代收集算法 (新生代,老年代,永久代)
新生代对象存活率低,就采用复制算法;老年代存活率高,就用标记清除算法或者标记整理算法。
- 新生代:一般情况下新生成的对象首先都是放在新生代的。新生代的目标就是尽可能快速的收集掉那些生命周期短的对象。新生代发生的GC频率较高。
- 老年代:在新生代中经历了多次(默认15次)垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
- 永久代:永久代主要存放静态文件,如 Java 类、方法等。
线程池
- new ThreadPoolExecutor
- 参数 核心线程数 最大线程数 保持活动时间 时间单位 饱和策略 linkedBlockQueue 任务队列 未满直接成为工作线程,达到核心线程,入队列,队列满了,直接执行,再满了就异常
Kotlin
空安全 类型推断 内联 数据类 反射 主构造函数 拓展函数 高阶函数 单例 注解 委托 泛型 协程
-
空安全,通过ide的编译提示,来避免null对象的调用,避免空指针异常,可以减少运行时的一些问题,这个变量在使用时是否可能为空
-
val只读变量,只能赋值一次,不能修改,但是可以通过钩子函数get修改返回值
-
int float这些基本数据类型在kotlin被抛弃了 Int Float
-
构造函数 constructor 主构造函数,在每个次级构造函数都要调用主构造函数
-
静态方法和静态变量等价写法 companion object 伴生对象,或者使用顶层声明,属性和函数隶属于package,不属于任何class
-
const 编译器常量
-
局部函数
-
Java的switch kotlin的when
-
== 值比较 ===内存地址比较
-
泛型 in(?extend)只用来输入 out (? super) 只用来输出
-
reified
-
高阶函数 参数或者返回值是函数类型的函数
-
匿名函数 其实是个对象,一个函数类型的对象,跟双冒号加函数名是一个东西
-
Lambda 也是一个函数类型的对象,Java中的lambda只是单函数接口一种便捷写法
-
3种函数类型的对象:双冒号加函数名,匿名函数,lambda
-
扩展函数,可以给已有的类额外添加函数和属性,不需要改源码和子类
-
内联函数 inline,减少高阶函数等函数类型的对象因临时创建对象导致的内存问题,比如频繁创建 crossinline:需要突破内联函数的不能间接调用参数的限制的时候
- inline: 通过内联(既函数内容直插到调用处)的方式来编译函数
- noinline: 局部的关闭这个优化,来摆脱不能把函数类型的参数当对象使用的限制
- crossinline:让内联函数里的函数类型的参数可以被间接的调用,代价是lambda表达式中使用return
-
作用域函数
- 作用域函数引用上下文对象有两种方式:
- 作为 lambda 表达式的接收者(this): run、with、apply
- 作为 lambda 表达式的参数(it): let、also
函数 对象引用 返回值 是否为扩展函数 使用场景 let it 表达式结果 是 1.非空执行 2. 替换局部变量优化代码阅读 run this 表达式结果 是 对象配置并计算结果 run - 表达式结果 否 需要表达式的地方执行代码块 apply this 上下文对象 是 对象配置 with this 表达式结果 否:把上下文对象当做参数 对这个对象进行的操作 also it 上下文对象 是 额外的操作 - 作用域函数引用上下文对象有两种方式:
-
协程
-
由kotlin官方提供一套线程API,可以不用过多的关心线程也可以方便的写出并发操作,就是一套线程框架
-
可以用看起来同步的方式写出异步代码,非阻塞式挂起,可以消除回调,简化代码可读性,Java不借助rxJava的话,要合并两个网络请求操作很麻烦,或者直接按先后顺讯调用,导致请求时间变长了,协程可以直接合并
launch(Dispatchers.Main) { val avatar = asyns { api.getAvatar(user)} val logo = asyns { api.getLogo(user)} val merged = suspendingMerge(avatar,logo) show(merged) }
-
launch(Dispatchers.IO) 开启一个协程,运行在io, withContext可以指定线程来执行,并且执行完后自动切回来
launch(Dispatchers.Main) { val image = withContext(Dispatchers.IO){ getImage(id) } show(image) }
-
优势可以在同一个代码块里进行多次线程切换
-
挂起 suspend 关键字,其实就是切个线程,只不过执行完成之后会自动切回来,一个稍后会被自动切回来的线程切换,suspend关键字只是个提醒,提醒调用者我是个耗时的函数,需要在协程里面调用,真正挂起的是代码块执行的代码,不是suspend关键字
-
非阻塞式挂起,不卡线程因为会做线程切换,代码看起来是阻塞的,其实是非阻塞式的
- 协程:切线程 挂起:就是可以自动切回来的切线程 非阻塞式:可以用看起来是阻塞的代码写出非阻塞式的操作
-
Android基础
Activity 启动模式
- 默认规则,在不同Task中打开同一个Activity,Activity会被创建多个实例,分别放进每一个Task
- task可以表示为系统最近任务列表,但是task被销毁了系统最近任务列表也可能会存在,但只是个残影,task内是没有了
- SingleTask 不管启动该Activity的是哪个task,都会创建放在自己的task,这种方式打开Activity的动画是应用之间切换的动画效果,如果该task内已经有了这个Activity,则会把该Activity上面的Activity销毁掉,并且回调onNewIntent,并不会重新创建,
- SingleInstance 会创建一个独立的task,该task只会有这么一个Activity,如果已经存在了,会直接复用,并且回调onNewIntent
- 在点击回退键时,SingleTask会在自己的App里进行回退,而SingleInstance会直接回到原先的APP
- SingleTop 会重用栈顶的Activity
- allowTaskReparenting,可以将Activity移动到相同affinity的task,但是Android9和10失效了,11又修复了
- affinity用来表示task的唯一性,如果singleInstance没有配置affinity,则退到后台最近任务可能会看不见该task,因为affinity冲突了,相同的taskAffinity最近任务列表只会显示最近的一个;singleTask则会先对比当前task的affinity,如果affinity相同,则入栈,不相同,则寻找相同affinity的task进行入栈,或者创建个新的task
Handler:线程中通信
- 四大元素Handler,Looper,Message,MessageQueue
- 很常见的运用场景,UI线程不能执行耗时操作,很可能会出现ANR,需要开子线程执行耗时的操作,一般来说不能在子线程 中更新UI(出于线程安全的问题不允许,除非在子线程结束后界面还没有显示出来,onCreate中执行子线程),此时需要更新UI的话就需要通过Handler来处理
- 使用的方式:在主线程中定义一个Handler,重写handMessage()方法,子线程拿到该handler的引用,通过sendMessage()来发送一个消息,在之前的handMessage()内处理消息
- Handler是如何工作的
- 在定义Handler之前,是需要有一个Looper和MessageQueue,MessageQueue用于存放消息的消息队列,Looper是用于轮询消息队列里的消息,从消息队列里不断的取消息交给handler处理,在主线程中并不需要我们自己定义Looper个MessageQueue是因为在ActivityThread中的main()已经帮我们初始化好了,如果在子线程定义handler,需要在定义之前执行Looper.prepare(),该方法 内部就是帮我们做初始化Looper以及MessageQueue还有一些其他的操作,而且该方法不能执行多次,否则报异常,说明一个线程中只能有一个Looper和一个MessageQueue。
- 在定义Handler之后需要调用Looper.loop(),用于启动轮询器,内部定义一个死循环,不断的从 MessageQueue里取消息
- 在handler.sendMessage()发送一个消息时,message会设置一个target,而这个target就是我们的handler对象,在通过looper取出消息,通过message设置的target,也就是handler对象,利用该handler的dispatchMessage()方法做了些简单的消息分发,其中有个就是分发给handMessage()处理,也就完成了消息的发送接收处理
- 其他异步消息机制的实现 handler.post(runable); —> 内部是通过sendMessageDelayed();
- sendMessageDelayed 能保证精准在规定时间上执行吗?不能,保证在这个时间之后执行,因为sendMessageDelayed最终将runnable入消息队列的时候,会产生一个when,而这个when在消息队列取出的时候,会验证当前系统时间是否比when大,大的话才会执行,说明如果在到了这个时间点的时候,Handler的正在执行一个任务,这个时候是无法直接处理这个延时任务,需要等到当前任务执行完,再从消息队列里next()的时候,才能执行
- View.post() —>通过activity内部的handler.post(); activity.runOnUiThread(); —>判断当前是不是在UI线程,如果是,直接执行,如果不是,通过handler.post()
- Looper每个线程只能有一个,原因是looper.prepare()的时候,会检测ThreadLocal里有没有looper,如果没有就会把looper放在ThreadLocal里面,有的话就报错了
- 主线程的死循环一直运行是不是特别消耗 CPU 资源呢,在轮询下一个发现没有消息时,在native层会对线程进行休眠,直到下一个消息到达时,会通过pipe管道机制来唤醒线程,这里采用的epoll机制,是一种 IO 多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质是同步I/O,即读写是阻塞的。
- postDelay 延迟是如何实现的,直接进入到队列,会有一个when的字段,内部会按时间顺序来排列,
事件分发机制
责任链模式
- Activity -> phoneWindow->ViewGroup->view
- dispatchTouchEvent 事件分发
- onInterceptTouchEvent 拦截事件 Activity和View是没有的
- onTouchEvent 处理事件
- 默认是一路走到底再回传事件
- 如果 onInterceptTouchEvent,返回True则表示自己需要拦截事件,就会走onTouchEvent,由onTouchEvent来判断是否消费该事件,如果不消费则交给子view来处理
- View中默认的事件处理,还包含了对onTouchListener的处理,在dispatchTouchEvent中,会判断onTouchListener,如果onTouchListener有设置,则不会触发onTouchEvent,没有设置的话则进入onTouchEvent,里面再是否可以点击,如果不可点击也就不消费事件,如果可点击做根据down和up的时间间隔来区分是点击还是长按,长按的话如果返回值是false的话,点击事件也是会响应的,因为长按没有消费掉
- 同一事件序列,如果onTouchEvent down事件没有消费,则不会收到其他事件,如果返回true,则后序事件move 和up会直接发到这个view的onTouchEvent,通过维护TouchTarget链表来实现的
- 当onInterceptTouchEvent返回True的时候,子View会收到一个cancel事件,通知子view这个事件序列你不要管了,需要把状态恢复
-
- 重写onTouchEvent写触摸反馈算法,并返回true
-
- 如果是ViewGroup可能和子view产生触摸判别冲突,可能需要重写onInterceptTouchEvent,在down事件中返回false,在合适的时候返回true拦截事件
-
- 如果子view希望阻止父view拦截,可以请求父view不要拦截的方法,仅针对当前事件流有效
View的绘制流程
事件分发机制是Android系统中非常重要的一部分,用于处理用户的触摸事件、键盘事件、手势事件等等。事件分发机制是指事件从触发源传递到事件处理对象的过程。下面是事件分发机制的详细描述:
首先,当用户触摸屏幕或按下键盘时,系统会将事件封装成一个MotionEvent或KeyEvent对象。这些事件对象包含了事件的类型、坐标、时间戳等信息。
接着,这些事件对象会先被传递给Activity的dispatchTouchEvent()或dispatchKeyEvent()方法。如果Activity的这些方法返回false,则事件会继续往下传递。
如果Activity返回true,则事件会在Activity内部进行处理,这时事件的传递就结束了。
如果Activity返回false,则事件会被传递到最上层的ViewGroup,即DecorView。
DecorView会将事件依次传递给它的子View,在传递事件之前,会先调用View的onInterceptTouchEvent()方法判断是否需要拦截事件并进行处理。
如果ViewGroup拦截了事件,则事件会被交给它的onTouchEvent()进行处理。
如果未拦截,则事件会继续往下传递到子View。
子View会先调用onTouchEvent()方法处理事件,如果返回false,则事件会向上传递到它的父View,依次重复上述过程。
如果子View返回true,则事件会被消费掉,传递停止。
最终,如果事件一直未被消费,则会传递到DecorView,在DecorView内部进行处理,如果也未被消费,则最终传递到Activity进行处理。
总之,事件分发机制实际上是一种链式的传递方式,采用自下而上的处理方式,最终由非常顶层的节点进行处理。开发者可以通过覆盖dispatchTouchEvent()等方法来自定义事件的处理过程。
measure–>layout–>draw 从父节点依次递归处理
-
measure meaureSpec 是父View对子View的尺寸限制,需要通过计算得出 ViewGroup.Layoutparams 定义宽高 MeasureSpec 32位int值, 最高2位是测量模式,后面的30是大小 模式:不确定,精确值,最大值
UNSPECIFIED :不对View进行任何限制,要多大给多大,一般用于系统内部 EXACTLY:对应LayoutParams中的match_parent和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值 AT_MOST :对应LayoutParams中的wrap_content。View的大小不能大于父容器的大小。
-
onMeasure 最终是调用设置最终大小
- 就是计算子view的位置和尺寸,以及自己的尺寸 measure->onMeasure->setMeasuredDimension
-
layout 自上而下遍历 根据子View的测量大小来摆放子View的位置 draw
-
onLayout
- 根据之前计算的结果来摆放子View
- invalidate()重新绘制
- requestLayout()布局发生变化, 重新测量,measure layout 不会回调draw
自定义ViewGroup
- onMeasure 就是计算子view的位置和尺寸,以及自己的尺寸
- 调用每个子view的measure方法来对子view进行测量
- measureSpec 根据ViewGroup的可用空间以及子view的LayoutParams计算得到子view的measureSpec,比如子view的宽度为matchParent,ViewGroup的Spec Mode 为AT_MOST或者EXACTLY,子view的mode就是EXACTLY;如果子view的warpParent,则不能超过ViewGroup,所以子View的是at_most
- 根据子view的测量结果,得出子View的位置,并且保存子view位置和尺寸
- 根据子view的位置和尺寸,计算出自身的位置,并且调用setMeasureDimension()保存
- 调用每个子view的measure方法来对子view进行测量
- onLayout 根据之前计算的结果来摆放子View
Bunder
ARN
发生ANR时会调用AppNotRespondingDialog.show()方法弹出对话框提示用户,查看信息 /data/anr/traces.txt, 它包含了应用所有的堆栈信息、CPU、IO等使用的情况等待
- 主线程被 IO 操作
- 主线程中存在耗时的计算
- 主线程中错误的操作,比如 Thread.wait 或者 Thread.sleep
- 应用在 5 秒内未响应用户的输入事件(如按键或者触摸)
- BroadcastReceiver 广播接收器在前台10s,后台60s的时间内没有响应完成
- Service 在前台20s,后台200s的时间内没有处理完成
- SP apply引发的ANR
- SP 调用 apply 方法,会创建一个等待锁放到 QueuedWork 中,并将真正数据持久化封装成一个任务放到异步队列中执行,任务执行结束会释放锁。Activity onStop 以及 Service 处理 onStop,onStartCommand 时,执行 QueuedWork.waitToFinish() 等待所有的等待锁释放。
- 所有此类 ANR 都是经由 QueuedWork.waitToFinish() 触发的,只要在调用此函数之前,将其中保存的队列手动清空即可。具体是Hook ActivityThrad的Handler变量,拿到此变量后给其设置一个Callback,Handler 的 dispatchMessage 中会先处理 callback。最后在 Callback 中调用队列的清理工作,注意队列清理需要反射调用 QueuedWork。
- ANR监控: 开启一个监控线程,在While循环里通过先设置一个监控状态,再用uiHandler去post runnable,Runable里面的任务是重置一下监控状态,然后该线程sleep 个5s,如果监控状态被重置了,说明post的runnable被执行了,主线程就没有卡住,如果没有被重置,则表示主线程发生了ANR
启动优化
- 线下检测: adb shell, totalTime
- 线上检测: 给每一个生命周期都打点,可以application的onCreate和onAttachBaseContext
- 第三方库异步初始化,整了一个初始化的task管理器,可以设置每个task的优先级,不是必要的库改为按需初始化,比如图片加载器等, 只有第一次用的时候才去初始化。有一些库是利用ContentProvider自动初始化的,就将它改为手动初始化,并且加入到task里
内存优化
- 内存抖动
- 短时间频繁创建销毁对象,回收内存的消耗,界面就更卡顿了
- for循环内部不要使用+号拼接字符串,每一次循环都会new,应该使用stringbuilder来拼接
- 布局利用include+mager.减少布局的层级,两层相同的布局可以减少
- viewstub(延时加载,比如在网络异常的界面,没必要在整个界面初始化的时候加载,要需要的时候加载就行,通过findViewById.inflate)
- 对象引用
- 软引用:内存空间足够,GC时就不会回收它;如果内存不足,就会回收这些对象的内存。可用来实现内存敏感的高速缓存
- 弱引用:扫描到只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存,但是可能需要运行多次GC,才能找到并释放弱引用对象,因为垃圾回收器是一个优先级很低的线程
- 减少不必要的内存开销
- 自动装箱: 把基础数据类型转换成对应的复杂类型,在自动装箱转化时,都会产生一个新的对象
- 内存复用:
- 资源复用:通用的字符串、颜色定义、简单页面布局的复用。
- 视图复用:可以使用ViewHolder实现ConvertView复用。
- 对象池:显示创建对象池,实现复用逻辑,对相同的类型数据使用同一块内存空间。
- Bitmap对象的复用:使用inBitmap属性可以告知Bitmap解码器尝试使用已经存在的内存区域,新解码的bitmap会尝试使用之前那张bitmap在heap中占据的pixel data内存区域。
- 使用最优的数据类型的
- HashMap与ArrayMap
- HashMap是一个散列链表,向HashMap中put元素时,先根据key的HashCode重新计算hash值,根据hash值得到这个元素在数组中的位置,如果数组该位置上已经存放有其它元素了,那么这个位置上的元素将以链表的形式存放,新加入的放在链头,最后加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。也就是说,向HashMap插入一个对象前,会给一个通向Hash阵列的索引,在索引的位置中,保存了这个Key对象的值。这意味着需要考虑的一个最大问题是冲突,当多个对象散列于阵列相同位置时,就会有散列冲突的问题。因此,HashMap会配置一个大的数组来减少潜在的冲突,并且会有其他逻辑防止链接算法和一些冲突的发生。
- ArrayMap提供了和HashMap一样的功能,但避免了过多的内存开销,方法是使用两个小数组,而不是一个大数组。并且ArrayMap在内存上是连续不间断的。
- 在ArrayMap中执行插入或者删除操作时,从性能角度上看,比HashMap还要更差一些,但如果只涉及很小的对象数,比如1000以下,就不需要担心这个问题了。因为此时ArrayMap不会分配过大的数组。
- 枚举类型
- 枚举最大的优点是类型安全,但在Android平台上,枚举的内存开销是直接定义常量的三倍以上。所以Android提供了注解的方式检查类型安全。目前提供了int型和String型两种注解方式:IntDef和StringDef,用来提供编译期的类型检查。
- Lur缓存:双链表,方便头部插入和尾部移除,get/set/sizeOf
- HashMap与ArrayMap
- 图片内存优化 (x * y * 编码格式)
- 设置位图的规格:一般使用ARGB444,要求不高的使用RGB565
- inSampleSize:进行缩放
- inScaled,inDensity和inTargetDensity实现更细的缩放图片:当inScaled设置为true时,系统会按照现有的密度来划分目标密度
- Lru缓存/Glide
我们都知道JVM拥有内存回收机制,自身会在虚拟机层面自动分配和释放内存,在Android的虚拟机中内存管理模型有一个分代(Generational Heap Memory )管理,当内存达到某一个阈值时,系统会根据不同的规则自动释放可以释放的内存。即便有了内存管理机制,但是,如果不合理地使用内存,也会造成一系列的性能问题,比如内存泄漏、内存抖动、短时间内分配大量的内存对象等等
内存泄露
- 单例模式 单例的生命周期跟应用的生命周期一样长,如果一个对象不使用了,单例又持有该对象的引用,就不能正常回收,最常见的,单例传了一个activity的context,如果是application的没什么问题
- 非静态内部类创建静态实例,因为非静态内部类默认持有外部类的引用,又用改非静态内部类创建了一个静态实例,就导致改静态实例一直持有改activity的引用
- 不影响层级深度的情况下,使用LinearLayout而不是RelativeLayout。因为RelativeLayout会让子View调用2次onMeasure,LinearLayout 在有weight时,才会让子View调用2次onMeasure。Measure的耗时越长那么绘制效率就低。 如果非要是嵌套,那么尽量避免RelativeLayout嵌套RelativeLayout
public class MainActivity extends AppCompatActivity {
private static TestResource mResource = null;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mManager == null)
{ mManager = new TestResource(); } //...
}
class TestResource { //... }
}
- handler 可能有延时消息还没处理,导致消息队列里的msg有handler实例的引用,handler有activity的引用,解决使用weakReference,在activity销毁时移除所有消息和runable
- 线程 直接activity new thread,如果再activity要销毁时子线程还没完成,导致不能销毁
- 资源对象没关闭
- 数据库操作没有关闭连接
- 动态注册广播没有取消注册.观察者也一样
- 输入输出流未关闭
- WebView,在ondestroy中未调用WebView.destroy
- 高频率执行,列如onDraw中创建对象
- 属性动画,无限循环动画,在destroy没有停止,view被动画持有,activity被view持有
内存泄漏怎么查找
- AS 的 AndroidProfiler 的内存工具,反复的打开和关闭一个页面,然后点击Profiler的GC按钮,查看内存有没有恢复到原来的数值,再点击heap dump查看当前的内存堆栈情况,找到测试的Activity,如果引用了多个实例,就内存泄漏了
- 使用MAT,
- 在开发阶段使用LeakCanary,编写完代码后可能有些资源没关闭或者是遗漏
卡顿优化
- 尽量减少View的层级,因为有的View可能会进行二次测量或者多次测量,比如LinerLayout 是包裹内容的,而子view内有个matchParent,就会先测量其它子view,然后从这些里面找到最大再次进行测量
- onDraw等高频的操作内,不要创建对象,频繁的创建和销毁会带来内存回收的消耗,导致页面卡顿
- 不要在主线程执行耗时的操作
- 线程优化,使用线程池,避免创建和销毁线程带来的开销
- 在主线程做轻微的耗时操作,比如读取数据库,读取SP,解析json,用该用异步加载
- TraceView 分析函数的调用过程,查看耗时,主要关注Calls + Recur Calls / Total和(该方法调用次数+递归次数)和Cpu Time / Call(该方法耗时)这两个值,然后优化这些方法的逻辑和调用次数,减少耗时。
- Android开发者模式下自带的Profile GPU Rendering,每一条柱状图都由红、黄、蓝、紫组成,分别对应每一帧在不同阶段的实际耗时。
- 卡顿监控:
- 利用主线程的消息队列处理机制,通过反射Loop里面的mLogging,通过自定义Printer,然后在Printer中获取到两次被调用的时间差,这个时间差就是执行时间。如果该时间超过阈值(如1000ms)时,主线程卡顿发生,并抛出各种有用信息,供开发者分析。
包体积优化
apk 主要组成: 第一部分是Dex,主要是class data 源码文件。 第二部分是Resource文件,主要是图片、xml、string等资源文件。 第三部分是Assets文件,主要存放一些类似签名摘要、音频、html默认文件等。 最后一部分是Native Library文件,主要是C++编写的so,其中lib下存放不同架构的so库。
打包流程 用aapt打包资源文件, 生成R.java类(资源索引表)、.arsc资源文件 和res文件 其次生成src资源文件编译产物,aidl files编译生成成java接口,并将R.java文件、工程源码文件、aidl.java文件, 通过javac合成.class文件。 最后.class文件、第三方jar和library,通过dx工具打包成Dex文件,最后是apk签名和apk对齐流程
- 混淆,Shrink
- 移除第三方sdk不需要用到的资源,利用gradle aapt2(Android Asset Packaging Tool)工具过滤
android { // ... aaptOptions { ignoreAssetsPattern "!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~" } }
- 图片压缩TinyPNG,比较大的图片改成了远程的
- 动态加载
直播聊天室优化
- 满500条移除前面的300条
- 间隔刷新,1s内的数据保存起来,然后统一刷新到ui上
- 1s内再增加一个最大数据量,如果达到了一定的数据量,也刷新到ui
- IM消息优化:有一条新消息时服务端只通知客户端有新消息了,然后由客户端发起请求,里面包含了客户端最近的一条消息id,告诉给服务端,然后服务器将客户端给的最后一条消息的id之后消息下发给客户端,这样可以保证不会丢消息,推拉机制
架构模式
-
MVC
- View:XML布局文件。 Model:实体模型(数据的获取、存储、数据状态变化)。 Controller:对应于Activity,处理数据、业务和UI。
-
MVP
- 过一个抽象的View接口(不是真正的View层)将Presenter与真正的View层进行解耦。Persenter持有该View接口,对该接口进行操作,而不是直接操作View层。这样就可以把视图操作和业务逻辑解耦,从而让Activity成为真正的View层。
- Presenter(以下简称P)层与View(以下简称V)层是通过接口进行交互的,接口粒度不好控制。粒度太小,就会存在大量接口的情况,使代码太过碎版化;粒度太大,解耦效果不好。同时对于UI的输入和数据的变化,需要手动调用V层或者P层相关的接口,相对来说缺乏自动性、监听性。如果数据的变化能自动响应到UI、UI的输入能自动更新到数据,那该多好!
- MVP是以UI为驱动的模型,更新UI都需要保证能获取到控件的引用,同时更新UI的时候要考虑当前是否是UI线程,也要考虑Activity的生命周期(是否已经销毁等)。
- MVP是以UI和事件为驱动的传统模型,数据都是被动地通过UI控件做展示,但是由于数据的时变性,我们更希望数据能转被动为主动,希望数据能更有活性,由数据来驱动UI。
- V层与P层还是有一定的耦合度。一旦V层某个UI元素更改,那么对应的接口就必须得改,数据如何映射到UI上、事件监听接口这些都需要转变,牵一发而动全身。如果这一层也能解耦就更好了。
- 复杂的业务同时也可能会导致P层太大,代码臃肿的问题依然不能解决。
-
MVVM
- 在常规的开发模式中,数据变化需要更新UI的时候,需要先获取UI控件的引用,然后再更新UI。获取用户的输入和操作也需要通过UI控件的引用。在MVVM中,这些都是通过数据驱动来自动完成的,数据变化后会自动更新UI,UI的改变也能自动反馈到数据层,数据成为主导因素。这样MVVM层在业务逻辑处理中只要关心数据,不需要直接和UI打交道,在业务处理过程中简单方便很多
- 使用官方的架构组件 ViewModel、LiveData、DataBinding 去实现MVVM
-
如何选择
- 如果项目简单,没什么复杂性,未来改动也不大的话,那就不要用设计模式或者架构方法,只需要将每个模块封装好,方便调用即可,不要为了使用设计模式或架构方法而使用。
- 对于偏向展示型的 app,绝大多数业务逻辑都在后端,app主要功能就是展示数据,交互等,建议使用 mvvm,数据驱动型
- 对于工具类或者需要写很多业务逻辑 app,使用mvp 或者mvvm都可。
签名机制
安装Apk的时候需要确保 APK 来源的真实性,以及 APK 没有被第三方篡改
-
v1
- 签名流程
- 对apk的所有条目生成摘要记录到mainfest.mf
- 计算mainfest.mf整个文件的消息摘要写入到*.sf文件
- 计算mainfest.mf的每一块的消息摘要写入到*.sf文件
- 计算*.sf的数字签名(先摘要再私钥加密)
- 将数字签名和 X.509 开发者数字证书(公钥)写入 *.RSA 文件
- 验证流程
- 取出 *.RSA 中包含的开发者证书
- 用证书中的公钥解密 *.RSA 中包含的签名,得到摘要
- 计算 *.SF 的摘要
- 对比证书里的摘要跟计算得到的是否一致
- 计算mainfest.mf的摘要
- 对比sf里记录的摘要与计算所得到的mainfest.mf是否一致
- 再用 MANIFEST.MF中的每一块数据去校验每一个文件是否被修改
- 缺点
- 完整性覆盖范围不足,比如META-INF
- 验证速度较差:验证程序必须解压所有压缩的条目,这需要花费更多时间和内存
- 签名流程
-
v2 (7.0以上)
-
签名流程
v2 签名方案不再以文件为单位计算摘要了,而是以 1 MB 为单位将文件拆分为多个连续的区块(chunk),每个分区的最后一个块可能会小于 1 MB; ZIP文件分为三块,条目内容区,中央目录区,中央目录结尾区 签名完成后会在条目区和目录区中间插入一个签名区块
- zip文件的三个区块,按1MB大小分割成多块
- 计算每一块的摘要
- 把这些分段的摘要再进行计算得到最终的摘要也就是 APK 的摘要
- 将 APK 的摘要 + 数字证书 + 其他属性生成签名数据写入到 签名区块
-
除了签名区块,其它3块是不能修改的,修改了就会导致校验不通过
-
渠道包信息可以写入到签名区块
-
-
v3 (9.0以上)
- Android 9.0 中引入了新的签名方式,它的格式大体和 v2 类似,在 v2 插入的签名块中,又添加了一个新快(Attr块)
- 在这个新块中,会记录我们之前的签名信息以及新的签名信息,以密钥转轮的方案,来做签名的替换和升级。这意味着,只要旧签名证书在手,我们就可以通过它在新的 APK 文件中,更改签名。
常用框架原理
如何选择第三方库
- 考虑生态,是不是经常维护,有哪些大厂落地
- 能不能解决现在的问题,学习的成本
- 对包体积的影响大不大
leakcanary
- 利用registerActivityLifecycleCallbacks监听Activity的生命周期
- RefWatcher.watch()创建了一个KeyedWeakReference用于去观察对象。
- 然后,在后台线程中,它会检测引用是否被清除了,并且是否没有触发GC。
- 如果引用仍然没有被清除,那么它将会把堆栈信息保存在文件系统中的.hprof文件里。
- HeapAnalyzerService被开启在一个独立的进程中,并且HeapAnalyzer使用了HAHA开源库解析了指定时刻的堆栈快照文件heap dump。
- 从heap dump中,HeapAnalyzer根据一个独特的引用key找到了KeyedWeakReference,并且定位了泄露的引用。
- HeapAnalyzer为了确定是否有泄露,计算了到GC Roots的最短强引用路径,然后建立了导致泄露的链式引用。
- 这个结果被传回到app进程中的DisplayLeakService,然后一个泄露通知便展现出来了。
在一个Activity执行完onDestroy()之后,将它放入WeakReference中,然后将这个WeakReference类型的Activity对象与ReferenceQueque关联。(作用在于Reference对象所引用的对象被GC回收时,该Reference对象将会被加入引用队列中(ReferenceQueue)的队列末尾,这相当于是一种通知机制.当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。)这时再从ReferenceQueque中查看是否有没有该对象,如果没有,执行gc,再次查看,还是没有的话则判断发生内存泄露了。最后用HAHA这个开源库去分析dump之后的heap内存。
OkHttp
- OkHttpClient:可以进行一些设置(超时时间等),发起请求,每个client都维护了自己的任列,连接池,缓存,拦截器
- Call,实际的一个请求,分同步和异步
- Dispatcher,调度器,任务队列,里面维护一个线程池,当收到call时,从线程池里构建一闲的线程来执行
- 拦截器,有一些系统的拦截器,比如重试的,缓存的,真正做网络请求的,还有开发者定义的拦截器,比如添加默认的Header和token,拦截器分两快,一个是request一个是response,负责请求前和请求后的拦截
- 优化:
- Connection连接池:http请求每次都会经历三次握手才能建立连接,通过连接池复用链接,减少握手消耗
- 请求线程池,核心线程数是0,也就是说线程池所有线程都会在闲置时进行回收,在不用的时候,不占用资源, 但是会有个KeepAlive的空闲时间,在空闲时又可以复用到线程,最大执行任务数并没有让线程池进行维护,而是自己建立了一个执行队列和就绪队列来进行维护 max64个
Glide
- with(),会验证当前线程,如果是在子线程,则使用的是application的context,如果是在主线程的话,则会根据context或者Fragment来判断,如果是application,则是全局的,如果是Activity或者Fragment,则会通过添加一个无UI的Fragment的形式,用来绑定生命周期
- into(),网络请求,图片解析,图片解码,bitmap生成,缓存处理,图片压缩,显示图片
- 缓存
- 弱引用缓存:将正在使用的图片
- 变换
插件化/组件化/热更新
- 组件化 组件化开发就是将一个app分成多个模块,组件化强调功能拆分,单独编译,单独开发,根据需求动态配置组件。
- 插件化 插件化是将一个apk根据业务功能拆分成不同的子apk,子apk可单独运行,插件化更关注动态加载、热更新。
- 热修复 热修复强调的是在不需要二次安装应用的前提下修复已知的bug。
插件化
实现原理
- 静态代理 dynamic-load-apk最早使用ProxyActivity这种静态代理技术,由ProxyActivity去控制插件中PluginActivity的生命周期
- 动态替换(HOOK) 在实现原理上都是趋近于选择尽量少的hook,并通过在manifest中预埋一些组件实现对四大组件的动态插件化。像Replugin。
- 容器化框架 VirtualApp能够完全模拟app的运行环境,能够实现app的免安装运行和双开技术。Atlas是阿里的结合组件化和热修复技术的一个app基础框架,号称是一个容器化框架。
实现插件化需要解决的问题
- 插件类的加载,解决宿主加载插件以及插件加载宿主的问题,classLoader/双亲委派
- 资源文件的加载,解决宿主和插件的资源文件的加载问题,以及资源合并和资源冲突的问题,AssetManager
- 合并式:addAssetPath时加入所有插件和主工程的路径;优点是插件和宿主可以相互访问,缺点是可能产生资源冲突。解决冲突:
- 修改aapt源码,定制aapt工具编译期间修改PP段
- 修改aapt的产物,即,编译后期重新整理插件Apk的资源,编排ID
- 独立式:各个插件只添加自己apk路径。不存在资源冲突,但是无法资源共享。
- 合并式:addAssetPath时加入所有插件和主工程的路径;优点是插件和宿主可以相互访问,缺点是可能产生资源冲突。解决冲突:
- 四大组件的支撑,支撑包括Activity,BroadReceiver,ContentProvider,Service四大组件在插件中的正常使用
- 代理模式(DL框架)将代理的Activity的生命周期同步给目标Activity,AIDL通信
- 坑位占用模式 replugin
VirtualApp
- 一个apk能够运行起来,关键在于有能够运行app的环境,va做的就是这个事情–构造一个能够运行app的环境。那这个环境是什么,代码上是framework,实际是android的一个核心进程是system server(system_process)。启动system server,会启动一系列一系列的核心服务,众所周知的例如ams,wms,pms等等等。va要做的就是虚拟一个system_process进程,里面也有这一系列的核心服务,也可以认为这个是system_process的一个影射。从代码上就知道,lody实现上,甚至是连函数的命名(例如各个service的systemReady方法),执行次序等都完全参照framework的代码,保证实现上接近system_process。
- 既然这个system_process是虚拟的,里面的服务也是虚拟的,那怎么run起来。其实这些服务什么也不干,就是调用对应的函数(例如虚拟进程里的ams收到startActivity intent请求,那就直接调用startActivity,当然还有很多细节要处理),最终还是由系统负责执行具体的行为没有任何底层hook的行为。
- 还有就是虚拟的app进程,app进程内关键就是替换掉进程内ServiceManager cache中的某些服务对象(ams,pms等的binder stub),还有通过Java动态代理hook一些binder stub的函数。
- 整个va基本都是Java一层实现的,主要技术就是反射,动态代理。至于jni一层,其实没有多少核心的东西,hook掉几个函数,例如opendexfile,因为dexfile的文件路径被改了,要指定到自己路径去。