2020年 中级Android面试总结
update time 2020年4月24日10点41分 ,阅读时间20分钟。主要收集在面试过程中普遍问到的基础知识(面试收集 主要来自于bilibili 嵩恒 蚂蚁金服等互联网公司)
TODO
- OkHttp中的线程池 参数理解 四种线程池理解 线程池参考文章
- ButterKnife 源码 原理了解(APT 编译注解)
- LeakCeary 源码 原理了解 (弱引用 队列监听对象 )
- 线程 进程 通信方式 / sleep whit 方法深入的学习
- 大厂面试必复习! JVM 存储JVM参考文章、GC垃圾清理 算法 GC算法参考文章
- Kotlin DSL 协程
- Glide 源码 原理了解 Glide参考博客
- 常见算法 (100道初中级题目 按照规律学习)
- MVVM JetPack
目录在简书平台不可用,误点!!!
网络 Java 基础
网络
网络模块 面试主要涉及到 应用层(HTTP DNS等) 和 传输层 (TCP UDP)的东西,
TCP UDP
TCP 连接 :传输可靠;有序;面向字节流;速度慢;较重量;全双工; 适用于文件传输、浏览器等
全双工:A 给 B 发消息的同时,B 也能给 A 发
半双工:A 给 B 发消息的同时,B 不能给 A 发
UDP 无连接 :传输不可靠;无序;面向报文;速度快;轻量; 适用于即时通讯、视频通话等
三次握手四次挥手
- 握手:
- 建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
- 服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
- 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成。
- 挥手:
- 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
- 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
- 客户端收到服务器的确认请求后,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
- 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
- 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
- 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。所以服务器结束TCP连接的时间要比客户端早一些。
HTTP HTTPS
HTTP 是超文本传输协议,明文传输;HTTPS 使用 SSL 协议对 HTTP 传输数据进行了加密
HTTP 默认 80 端口;HTTPS 默认 443 端口
优点:安全
缺点:费时、SSL 证书收费,加密能力还是有限的,但是比 HTTP 更加安全,也是大势所趋。
Get 参数放在 url 中;Post 参数放在 request Body 中
Get 可能不安全,因为参数放在 url 中,并且对于传送的数据长度有限制。
JVM
内存模型
- 栈:储存局部变量 (线程私有 使用完毕就会释放)
- 堆:储存 new 出来的东西 成员方法等 (线程共享 使用完毕 等待gc清理)
- 方法区: 对象的运行过程 (线程共享)
- 本地方法区:为系统方法使用 (线程私有)
- 寄存器:为CPU提供
- 程序计数器:指向当前线程正在执行的指令的地址,确保多线程下正常运行(线程私有)
Jvm 调用方法过程
- 在运行前确认方法中的 局部变量的数量、操作栈大小 和 方法参数数量
- JVM在执行方法中 会将 局部变量,操作数,方法返回地址等信息 保存在 栈帧中,一个线程中 只有位于顶部的栈帧处于活动中
GC清理 及 清理算法
- 程序计数法
- 程序可达性
清理算法
- 复制回收算法 :内存使用率50%,只使用一半内存, 地址连贯,多用于新生代。
- 标记清除算法:使用率100%,地址碎片化,多用于老年代
- 标记整理算法:在标记清除基础上,对于存活对象进行整理,多用于碎片较多的老年代。
线程 进程
进程:进程是系统进行资源分配和调度的一个独立单位 (拥有独立内存空间),一个app就是一个进程。
线程:是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一些在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
Android 进程通讯方式
-
bundle : 由于Activity,Service,Receiver都是可以通过Intent来携带Bundle传输数据的,所以我们可以在一个进程中通过Intent将携带数据的Bundle发送到另一个进程的组件。(bundle只能传递三种类型,一是键值对的形式,二是键为String类型,三是值为Parcelable类型)
-
ContentProvider :ContentProvider是Android四大组件之一,以表格的方式来储存数据,提供给外界,即Content Provider可以跨进程访问其他应用程序中的数据
-
文件 :两个进程可以到同一个文件去交换数据,我们不仅可以保存文本文件,还可以将对象持久化到文件,从另一个文件恢复。要注意的是,当并发读/写时可能会出现并发的问题。
-
Broadcast :Broadcast可以向android系统中所有应用程序发送广播,而需要跨进程通讯的应用程序可以监听这些广播。
-
AIDL :AIDL通过定义服务端暴露的接口,以提供给客户端来调用,AIDL使服务器可以并行处理。
-
Messager :Messenger封装了AIDL之后只能串行运行,所以Messenger一般用作消息传递
-
Socket
Android 线程通信
Handler 和 AsyncTask (AsyncTask:异步任务,内部封装了Handler)
sleep wait 等 区别
-
wait(): 当一个线程执行到wait()方法时,它就进入到一个等待池中,同时释放对象锁,使得其他线程可以访问。用户可以使用notify,notifyAll或者指定睡眠时间来唤醒当前等待池中的线程。
-
sleep():该函数是Thread的静态函数,作用是使调用线程进入睡眠状态。因为sleep()是Thread类的Static方法,因为它不能改变对象的机制。所以,当在一个Synchronized块中调用sleep方法时,线程虽然休眠了,但是对象的机制并没有被释放,其他线程无法访问这个对象
注意:wait方法,notify方法,notifyAll方法必须放在synchronized block中,否则会抛出异常。
-
join():等待目标线程执行完成之后再继续执行
-
yield():线程礼让。目标线程由运行状态转换为就绪状态,也就是让出执行权限,让其他线程得以优先执行,但其他线程能否优先执行是未知的。
数据结构
ArrayList 、LinkedList 和SparseArray
-
ArrayList :基于动态数组实现,所以查找快(O1) 增删慢 (On),如果创建方法无参数,默认数组长度为10,添加元素如果需要扩容则是新创建一个 数组,进行数据的转移拷贝。删除的时候 如果删除成功,后续元素需要通过System.arraycopy 进行元素移动(相当的低效)。改查 则比较迅速。
-
LinkedList : 底层基于双向链表,维持了 last 和 next 两个节点,所以 查找慢 (On) 增删快(O1),链表的查找是循环的(不过查找的过程是 先判断index是靠近哪一段 然后再进行查找 可以理解为 O(n/2)),但是速度还是慢。在添加和删除中,因为添加是直接放到链表尾部 但是删除存在 先循环一遍,然后删除的情况,不过 相对于ArrayList的复制要好的很多了。
-
SparseArray : key 只能为整数型,内部也是两个数组,一个存key,一个存val , 添加的时候 key 不用装箱,val则需要如果是基本类型,查找key还是用的二分法查找。也就是说它的时间复杂度还是O(logN)
HashMap HashTable 和 ArrayMap
-
HashMap : jdk8后由数组、链表和红黑树。当数组中的数据出现 hash冲突的时候启动链表,当链表中的 个数超过8个 将链表转为红黑树。允许 key val 为NULL,key和 val存储数据可以为任何类型(非基本类型 会自动装箱)。HashMap并不是线程安全的。
-
HashTable : 其实主体和 HashMap类似,但是写入和 读取方法 添加了 synchronize 可以做到 线程安全,但是效率没有HashMap高。
-
ArrayMap :key val 可以为 任意类型(非基本类型,会自动装箱),中有两个数组,一个存储key 的hash值,另外一个交叉存储 key val 数据(key val key val .... 形式存储),
设计模式
多线程
锁问题
可重入锁 :已经获取到锁后,再次调用同步代码块/尝试获取锁时不必重新去申请锁,可以直接执行相关代码。 ReentrantLock 和 synchronized 都是可重入锁
公平锁 : 等待时间最久的线程会优先获得锁 , synchronized lock 默认都为非公平锁
锁主要分为:悲观锁 (线程一旦得到锁,其他线程就挂起等待。用于写入频繁的 synchronized)和 乐观锁(假设没有冲突,不加锁,更新数据时判断该数据是否过期,过期的话则不进行数据更新,适用于读取操作频繁的场景,比如 AtomicInteger、AtomicLong、AtomicBoolean)
锁的状态依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
-
volatile :只能用来修饰变量,适用修饰可能被多线程同时访问的变量,相当于轻量级的synchronized,保证有序性和 可见性(变量位于主内存中,每个线程还有自己的工作内存,变量在自己线程的工作内存中有份拷贝,线程直接操作的是这个拷贝被 volatile 修饰的变量改变后会立即同步到主内存)。
-
synchronized :是java 的关键字(悲观锁),自动释放锁,并且无法中断锁。保证 原子性、可见性 和 有序性。
-
Lock : java 中的一个接口,lock 需要手动释放,所以需要写到 try catch 块中并在 finally 中释放锁,可以手动中断锁。
双重检查单例加volatile的原因 : 将instance =newInstance(); 创建实例分为三个过程 ,1.分配内存 2.初始化 3.将instance指向分配的内存空。但是如果 在另一个线程中想要使用instance,发现instance!=null,但是实际上instance还未初始化完毕这个问题。
synchronized 原理
Synchronized可以把任何一个非null对象作为"锁",其中都有一个监视器锁:monitor。Synchronized的语义底层是通过一个monitor的对象来完成。
- 同步代码块(synchronize(this)的方式)会执行 monitorenter 开始,motnitorexit 结束,当线程进入monitor,如果为0 则改为1 并分配使用,如果为1 则需要挂起等待。其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
- 同步代码块采用( synchronized void method()的方式)调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
synchronized 在JDK 1.6以后的优化
-
自适应自旋锁 :指一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放(自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定),而不是进入线程挂起或睡眠状态(因为为了很短的等待时间就去挂起唤醒会 更低效)。
-
锁消除:但是在有些情况下,JVM检测到不存在共享数据竞争,JVM会对这些同步锁进行锁消除。
-
锁粗化 :就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
-
偏向锁、轻量级锁:轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时通过标识,避免再走各种加锁/解锁流程,达到进一步提高性能。
synchronized 添加在非静态方法上为 对象锁,如果在静态方法上为类锁。
ClassLoader
类加载过程
- 加载:获取类的二进制字节流;生成方法区的运行时存储结构;在内存中生成 Class 对象
- 验证:确保该 Class 字节流符合虚拟机要求
- 准备:初始化静态变量
- 解析:将常量池的符号引用替换为直接引用
- 初始化:执行静态块代码、类变量赋值
- 使用
- 卸载
Android
常用设计模式及源码使用
-
单例模式
初始化比较复杂,并且程序中只需要一个。避免重复创建消耗内存
Android中 获取WindowManager服务引用 WindowManager wm = (WindowManager)getSystemService(getApplication().WINDOW_SERVICE);
l另外一种不错实现单例的方式 使用 eunm,
public class Singleton {
private static volatile Singleton s;
private Singleton(){};
public static Singleton getInstance() {
if(s == null) {
synchronized (Singleton.class) {
if(s == null) {
s = new Singleton();
}
}
}
return s;
}
}
-
创建者模式
创建某对象时,需要设定很多的参数(通过setter方法),但是这些参数必须按照某个顺序设定
Android 中 创建所有的 Dialog 中使用的
public class TestClient {
private int index;
private String name;
public TestClient() {
this(new Builder());
}
public TestClient(Builder builder){
this.index = builder.index;
this.name = builder.name;
}
public static final class Builder {
private int index;
private String name;
public Builder() {
this.index = 1;
this.name = "xxx";
}
public Builder(TestClient testClient){
this.index = testClient.index;
this.name = testClient.name;
}
public Builder setIndex(int index) {
this.index = index;
return this;
}
public Builder setName(String name) {
this.name = name;
return this;
}
public TestClient build(){
return new TestClient(this);
}
}
}
- 原型模式
-
工厂模式
定义一个创建对象的工厂,根据不同传参 创建不同的对象。
Android 中 BitmapFactory 和 Iterator 根据循环对象不同返回不同的对象 -
策略模式
有一系列的算法,将算法封装起来(每个算法可以封装到不同的类中),各个算法之间可以替换,策略模式让算法独立于使用它的客户而独立变化
Android 中的 时间插值器,可以使用不同的 加速 减速 或者自定义加速器 展示不同的动画效果 -
责任链模式
使多个对象都有机会处理请求,从而避免请求的发送者和接受者直接的耦合关系,将这些对象连成一条链,并沿这条链传递该请求,直到有对象处理它为止。
Android 中有 View 点击事件分发 或者 第三方库 OKHttp 中的拦截器 -
命令模式
命令模式将每个请求封装成一个对象,从而让用户使用不同的请求把客户端参数化;将请求进行排队或者记录请求日志,以及支持可撤销操作。
Android 事件机制中,底层逻辑对事件的转发处理。每次的按键事件会被封装成NotifyKeyArgs对象,通过InputDispatcher封装具体的事件操作 / Runable实现中封装我们需要的实现 -
观察者模式
Java的Observable类和Observer接口就是实现了观察者模式。一个Observer对象监视着一个Observable对象的变化,当Observable对象发生变化时,Observer得到通知,就可以进行相应的工作。 -
中介者模式
在Binder机制中,即ServiceManager持有各种系统服务的引用 ,当我们需要获取系统的Service时,首先是向ServiceManager查询指定标示符对应的Binder,再由ServiceManager返回Binder的引用。并且客户端和服务端之间的通信是通过Binder驱动来实现,这里的ServiceManager和Binder驱动就是中介者。 -
代理模式
给某一个对象提供一个代理,并由代理对象控制对原对象的引用 (,静态代理 和 动态代理) -
适配器模式
把一个类的接口变换成客户端所期待的另一个接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
线程池
启动模式
- standard 标准模式
- singleTop 栈顶复用模式 (例如:推送点击消息界面)
- singleTask 栈内复用模式 (例如:首页)
- singleInstance 单例模式 (单独位于一个任务栈中,例如:拨打电话界面)
序列化
- Serializable : Java 序列化方式,适用于存储和网络传输,serialVersionUID 用于确定反序列化和类版本是否一致,不一致时反序列化回失败
- Parcelable :Android 序列化方式,适用于组件通信数据传递,性能高,因为不像 Serializable 一样有大量反射操作,频繁 GC
进程
IPC 进程通讯方式
- Intent 、Bundle : 要求传递数据能被序列化,实现 Parcelable、Serializable ,适用于四大组件通信。
- 文件共享 :适用于交换简单的数据实时性不高的场景。
- AIDL:AIDL 接口实质上是系统提供给我们可以方便实现 BInder 的工具
- Messenger:基于 AIDL 实现,服务端串行处理,主要用于传递消息,适用于低并发一对多通信
- ContentProvider:基于 Binder 实现,适用于一对多进程间数据共享(通讯录 短信 等)
- Socket:TCP、UDP,适用于网络数据交换
进程保活
进程被杀原因:1.切到后台内存不足时被杀;2.切到后台厂商省电机制杀死;3.用户主动清理
保活方式:
- Activity 提权:挂一个 1像素 Activity 将进程优先级提高到前台进程
- Service 提权:启动一个前台服务(API>18会有正在运行通知栏)
- 广播拉活 (监听 开机 等系统广播)
- Service 拉活
- JobScheduler 定时任务拉活 (android 高版本不行)
- 双进程拉活
- 监听其他大厂 广播 (tx baidu 全家桶互相拉)
Hook
Hook 的选择点:静态变量和单例,因为一旦创建对象,它们不容易变化,非常容易定位。
Hook 过程:
寻找 Hook 点,原则是静态变量或者单例对象,尽量 Hook public 的对象和方法。
选择合适的代理方式,如果是接口可以用动态代理。
偷梁换柱——用代理对象替换原始对象。
多数插件化 也使用的 Hook技术
内存泄漏
- 构造单例的时候尽量别用Activity的引用;
- 静态引用时注意应用对象的置空或者少用静态引用;
- 使用静态内部类+软引用代替非静态内部类;
- 及时取消广播或者观察者注册;耗时任务、属性动画在Activity销毁时记得cancel;
- 文件流、Cursor等资源及时关闭;
- Activity销毁时WebView的移除和销毁。
View
Window WindowManager WMS
-
Window :抽象类 不是实际存在的,而是以 View 的形式存在,通过 PhoneWindow 实现 (PhoneWindow = DecorView = Title + ContentView)
-
WindowManager:外界访问 Window 的入口 管理Window 中的View , 内部通过 Binder 与 WMS IPC 进程交互
-
WMS:管理窗口 Surface 的布局和次序,作为系统级服务单独运行在一个进程
-
SurfaceFlinger:将 WMS 维护的窗口按一定次序混合后显示到屏幕上
activity启动流程
![](https://img.haomeiwen.com/i6188347/341cf6c33be1e363.jpg)
View 工作流程
通过 SetContentView(),调用 到PhoneWindow ,后实例DecorView ,通过 LoadXmlResourceParser() 进行IO操作 解析xml文件 通过反射 创建出View,并将View绘制在 DecorView上,这里的绘制则交给了ViewRootImpl 来完成,通过performTraversals() 触发绘制流程,performMeasure 方法获取View的尺寸,performLayout 方法获取View的位置 ,然后通过 performDraw 方法遍历View 进行绘制。
事件分发
一个 MotionEvent 产生后,按 Activity -> Window -> DecorView(ViewGroup) -> View 顺序传递,View 传递过程就是事件分发,因为开发过程中存在事件冲突,所以需要熟悉流程:
- dispatchTouchEvent:用于分发事件,只要接受到点击事件就会被调用,返回结果表示是否消耗了当前事件
- onInterceptTouchEvent:用于判断是否拦截事件(只有ViewGroup中存在),当 ViewGroup 确定要拦截事件后,该事件序列都不会再触发调用此 ViewGroup 的 onIntercept
- onTouchEvent:用于处理事件,返回结果表示是否处理了当前事件,未处理则传递给父容器处理。(事件顺序是:OnTouchListener -> OnTouchEvent -> OnClick)
View.post
想要在 onCreate 中获取到View宽高的方法有:
- ViewTreeObserver 监听界面绘制事件,在layout时调用,使用完毕后记得removeListener
- 就是View.post
源码分析:
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action);
return true;
}
View.post(runable) 通过将runable 封装为HandlerAction对象,如果attachInfo为null 则将Runnable事件 添加到等待数组中, attachInfo初始化是在 dispatchAttachedToWindow 方法,置空则是在detachedFromWindow方法中,所以在这两个方法生命周期中,调用View.post方法都是直接让 mAttachInfo.handler 执行。
ViewRootImpl.class
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
context);
final ViewRootHandler mHandler = new ViewRootHandler();
通过查找 mAttachInfo.handler 是在主线程中声明的,没有传参则 Looper 为主线程Looper,所以在View.post中可以更新UI。
但是为什么可以再View.post()中获取控件尺寸呢?
android 运行是消息驱动,通过源码 可以看到 ViewRootImpl 中 是先将 TraversalRunnable添加到 Handler 中运行的 之后 才是 View.post()
ViewRootImpl.class
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
// 该方法之后才有 view.post()
performTraversals();
...
}
}
因此,这个时候Handler正在执行着TraversalRunnable这个Runnable,而我们post的Runnable要等待TraversalRunnable执行完才会去执行,而TraversalRunnable这里面又会进行measure,layout和draw流程,所以等到执行我们的Runnable时,此时的View就已经被measure过了,所以获取到的宽高就是measure过后的宽高。
动画
- 帧动画 :AnimationDrawable 实现,在资源文件中存放多张图片,占用内存多,容易OOM
- 补间动画 :作用对象只限于 View 视觉改变,并没有改变View 的 xy 坐标,支持 平移、缩放、旋转、透明度,但是移动后,响应时间的位置还在 原处,补间动画在执行的时候,直接导致了 View 执行 onDraw() 方法。补间动画的核心本质就是在一定的持续时间内,不断改变 Matrix 变换,并且不断刷新的过程。
- 属性动画 :ObjectAnimator、ValuetAnimator、AnimatorSet 可以是任何View,动画选择也比较多,其中包含 差速器,可以控制动画速度,节奏。类型估值器 可以根据当前属性改变的百分比计算改变后的属性值 。因为ViewGroup 在 getTransformedMotionEvent方法中通过子 View 的 hasIdentityMatrix() 来判断子 View 是否经过位移之类的属性动画。调用子 View 的 getInverseMatrix() 做「反平移」操作,然后判断处理后的触摸点是否在子 View 的边界范围内。
提升动画 可以打开 硬件加速,使GPU 承担一部分CPU的工作。
Handler线程间通信
作用:线程之间的消息通信
流程:主线程默认实现了Looper (调用loop.prepare方法 向sThreadLocal中set一个新的looper对象, looper构造方法中又创建了MsgQueue) 手动创建Handler ,调用 sendMessage 或者 post (runable) 发送Message 到 msgQueue ,如果没有Msg 这添加到表头,有数据则判断when时间 循环next 放到合适的 msg的next 后。Looper.loop不断轮训Msg,将msg取出 并分发到Handler 或者 post提交的 Runable 中处理,并重置Msg 状态位。回到主线程中 重写 Handler 的 handlerMessage 回调的msg 进行主线程绘制逻辑。
问题:
- Handler 同步屏障机制:通过发送异步消息,在msg.next 中会优先处理异步消息,达到优先级的作用
- Looper.loop 为什么不会卡死:为了app不挂掉,就要保证主线程一直运行存在,使用死循环代码阻塞在msgQueue.next()中的nativePollOnce()方法里 ,主线程就会挂起休眠释放cpu,线程就不会退出。Looper死循环之前,在ActivityThread.main()中就会创建一个 Binder 线程(ApplicationThread),接收系统服务AMS发送来的事件。当系统有消息产生(其实系统每 16ms 会发送一个刷新 UI 消息唤醒)会通过epoll机制 向pipe管道写端写入数据 就会发送消息给 looper 接收到消息后处理事件,保证主线程的一直存活。只有在主线程中处理超时才会让app崩溃 也就是ANR。
- Messaage复用: 将使用完的Message清除附带的数据后, 添加到复用池中 ,当我们需要使用它时,直接在复用池中取出对象使用,而不需要重新new创建对象。复用池本质还是Message 为node 的单链表结构。所以推荐使用Message.obation获取 对象。
Android 和WebView 通信
-
js调用android :
// myObj 为在js中使用的对象名称 JavaScriptInterfaces 是我们自定义的一个类
webView.addJavascriptInterface(new JavaScriptInterfaces(), "myObj");
myObj.xx() //js方法
-
android 调用js
无参数mWebView.loadUrl("javascript:wave()");
有参数
webView.evaluateJavascript(String.format("javascript:callH5Re('测试数据')"), new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
Log.e("Test", "onReceiveValue: "+value );
} });
app优化 (项目中处理的一些难点)
主要分为 启动优化,布局优化 ,打包优化 等
启动优化
- 闪屏页 优化,设置theme 默认欢迎背景
- 懒加载 第三方库,不要都放在application 中初始化
- 如果项目中有 webview ,可以提前在app空闲时间加载 webview 的内核,如果多处使用 可以创建缓存池,缓存webview,
- 如果android 5.0- 在applicaton 的 attchbaseContext() 中加载MultiDex.install 会更加耗时,可以采用 子线程(子线程加载 需要担心ANR 和ContentProvider 未加载报错的问题)或者单独开一个进程B,进程B开启子线程运行MultiDex.install ,让applicaton 进入while 循环等待B进程加载结果。
MultiDex 优化,apk打包分为 android 5.0 + 使用 ART虚拟机 不用担心
布局UI优化
看过布局绘制源码流程后,可以知道 setContextView中 在ViewRootImpl 中使用 pull 的方法(这里可以扩展xml读取方式 SAX :逐行解析、dom:将整个文件加载到内存 然后解析,不推荐、pull:类似于 SAX 进行了android平台的优化,更加轻量级 方便)迭代读取 xml标签,然后对view 进行 measure,layout 和draw 的时候都存在耗时。通常优化方式有:
- 减少UI层级、使用merge、Viewstub标签 优化重复的布局
- 优化 layout ,尽量多使用ConstraintLayout,因为 relalayout 和 linearlayout 比重的情况下都存在多次测量
- recyclerView 缓存 ( 可扩展 说明 rv的缓存原理 )
- 比较极端的 将 measure 和 layout 放在子线程,在主线程进行draw。或者 子线程中 加载view 进行IO读取xml,通过Handler 回调主线程 加载view(比如android 原生类 AsyncLayoutInflate )
- 将xml直接通过 第三方工具(原理 APT 注解 翻译xml)直接将xml 转为 java代码
更多UI优化文章
打包优化
Analyze APK 后可以发现代码 和 资源其实是 app包的主要内存
- res 文件夹下 分辨率下的图片 国内基本提供 xxhdpi 或者 xhdpi 即可,android 会分析手机分辨率到对应分辨率文件夹下加载资源
- res中的 png 图片 都可以转为 webg 或者 svg格式的 ,如果不能转 则可以通过 png压缩在减少内存
- 通过在 build.gradle 中配置 minifyEnabled true(混淆)shrinkResources true (移除无用资源)
- Assests 中的 mp4 /3 可以在需要使用的时候从服务器上下载下来,字体文件 使用字体提取工具FontZip 删除不用的文字格式,毕竟几千个中文app中怎么可能都使用
- lib 包如果 适配机型大多为高通 RAM ,可以单独引用abiFilters "armeabi-v7a"
- build文件中 resConfigs "zh" 剔除掉 官方中或者第三方库中的 外国文字资源
第三方库 源码总结
LeakCanary 原理
通过 registerActivityLifecycleCallbacks 监听Activity或者Fragment 销毁时候的生命周期(如果不想那个对象被监控则通过 AndroidExcludedRefs 枚举,避免被检测)
public void watch(Object watchedReference, String referenceName) {
if (this == DISABLED) {
return;
}
checkNotNull(watchedReference, "watchedReference");
checkNotNull(referenceName, "referenceName");
final long watchStartNanoTime = System.nanoTime();
String key = UUID.randomUUID().toString();
retainedKeys.add(key);
final KeyedWeakReference reference =
new KeyedWeakReference(watchedReference, key, referenceName, queue);
ensureGoneAsync(watchStartNanoTime, reference);
}
然后通过弱引用和引用队列监控对象是否被回收(弱引用和引用队列ReferenceQueue联合使用时,如果弱引用持有的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。即 KeyedWeakReference持有的Activity对象如果被垃圾回收,该对象就会加入到引用队列queue)
void waitForIdle(final Retryable retryable, final int failedAttempts) {
// This needs to be called from the main thread.
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override public boolean queueIdle() {
postToBackgroundWithDelay(retryable, failedAttempts);
return false;
}
});
}
IdleHandler,就是当主线程空闲的时候,如果设置了这个东西,就会执行它的queueIdle()方法,所以这个方法就是在onDestory以后,一旦主线程空闲了,就会执行一个延时五秒的子线程任务,任务:检测到未被回收则主动 gc ,然后继续监控,如果还是没有回收掉,就证明是内存泄漏了。 通过抓取 dump文件,在使用 第三方 HAHA 库 分析文件,获取到到达泄露点最近的线路,通过 启动另一个进程的 DisplayLeakService 发送通知 进行消息的展示。
OkHttp
同步和异步 网络请求使用方法
// 同步get请求
OkHttpClient okHttpClient=new OkHttpClient();
final Request request=new Request.Builder().url("xxx").get().build();
final Call call = okHttpClient.newCall(request);
try {
Response response = call.execute();
} catch (IOException e) {
}
//异步get请求
OkHttpClient okHttpClient=new OkHttpClient();
final Request request=new Request.Builder().url("xxx").get().build();
final Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
// 异步post 请求
OkHttpClient okHttpClient1 = new OkHttpClient();
RequestBody requestBody = new FormBody.Builder()
.add("xxx", "xxx").build();
Request request1 = new Request.Builder().url("xxx").post(requestBody).build();
okHttpClient1.newCall(request1).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
同步请求流程:
通过OkHttpClient new生成call实例 Realcall
Dispatcher.executed() 中 通过添加realcall到runningSyncCalls队列中
通过 getResponseWithInterceptorChain() 对request层层拦截,生成Response
通过Dispatcher.finished(),把call实例从队列中移除,返回最终的response
异步请求流程:
生成一个AsyncCall(responseCallback)实例(实现了Runnable)
AsyncCall通过调用Dispatcher.enqueue(),并判断maxRequests (最大请求数)maxRequestsPerHost(最大host请求数)是否满足条件,如果满足就把AsyncCall添加到runningAsyncCalls中,并放入线程池中执行;如果条件不满足,就添加到等待就绪的异步队列,当那些满足的条件的执行时 ,在Dispatcher.finifshed(this)中的promoteCalls();方法中 对等待就绪的异步队列进行遍历,生成对应的AsyncCall实例,并添加到runningAsyncCalls中,最后放入到线程池中执行,一直到所有请求都结束。
责任链模式 和 拦截器
责任链
源码跟进 execute() 进入到 getResponseWithInterceptorChain() 方法
Response getResponseWithInterceptorChain() throws IOException {
//责任链 模式
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
chain.proceed() 方法核心代码。每个拦截器 intercept()方法中的chain,都在上一个 chain实例的 chain.proceed()中被初始化,并传递了拦截器List与 index,调用interceptor.intercept(next),直接最后一个 chain实例执行即停止。
//递归循环下一个 拦截器
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Call call = realChain.call();
EventListener eventListener = realChain.eventListener();
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
while (true) {
...
// 循环中 再次调用了 chain 对象中的 proceed 方法,达到递归循环。
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
...
}
}
拦截器
- RetryAndFollowUpInterceptor :重连并跟踪 拦截器。
- BridgeInterceptor : 将用户请求构建为网络请求(hander cooker content-type 等) 并发起请求 。
- CacheInterceptor : 缓存拦截器 负责从缓存中返回响应和把网络请求响应写入缓存。
- ConnectInterceptor : 与服务端 建立连接,并且获得通向服务端的输入和输出流对象。
OkHttp 流程
- 采用责任链方式的拦截器,实现分成处理网络请求,可更好的扩展自定义拦截器(采用GZIP压缩,支持http缓存)
- 采用线程池(thread pool)和连接池(Socket pool)解决多并发问题,同时连接池支持多路复用(http2才支持,可以让一个Socket同时发送多个网络请求,内部自动维持顺序.相比http只能一个一个发送,更能减少创建开销))
- 底层采用socket和服务器进行连接.采用okio实现高效的io流读写
ButterKnife
butterKnife 使用的是 APT 技术 也就是编译时注解,不同于运行时注解(在运行过程中通过反射动态地获取相关类,方法,参数等信息,效率低),编译时注解 则是在代码编译过程中对注解进行处理(annotationProcessor技术),通过注解获取相关类,方法,参数等信息,然后在项目中生成代码,运行时调用,其实和直接手写代码一样,没有性能问题,只有编辑时效率问题。
ButterKnife在Bind方法中 获取到DecorView,然后通过Activity和DecorView对象获取xx_ViewBinding类的构造对象,然后通过构造方法反射实例化了这个类 Constructor。
在编写完demo之后,需要先build一下项目,之后可以在build/generated/source/apt/debug/包名/下面找到 对应的xx_ViewBinding类,查看bk 帮我们做的事情,
xx_ViewBinding.java
@UiThread
public ViewActivity_ViewBinding(ViewActivity target, View source) {
this.target = target;
target.view = Utils.findRequiredView(source, R.id.view, "field 'view'");
}
Utils.java
public static View findRequiredView(View source, @IdRes int id, String who) {
View view = source.findViewById(id);
if (view != null) {
return view;
}
String name = getResourceEntryName(source, id);
throw new IllegalStateException("Required view ...."}
通过上述上述代码 可以看到 注解也是帮我们完成了 findviewbyid 的工作。
butterknife 实现流程
- 扫描Java代码中所有的ButterKnife注解
- 发现注解, ButterKnifeProcessor会帮你生成一个Java类,名字<类名>$$ViewBinding.java,这个新生成的类实现了Unbinder接口,类中的各个view 声明和添加事件都添加到Map中,遍历每个注解对应通过JavaPoet生成的代码。
Rxjava 2
切换到子线程用的 线程池 ,切换到主线程则用的Handler
算法
冒泡
public static void bubble(int[] arr){
int temp;
//根据角标进行比较,
for(int i = 0; i<arr.length; i++){
//j是数组的最后一个角标
for (int j = arr.length-1; j > i; j--) {
if (arr[j] < arr[j - 1]) {
//从后往前进行比较,小数往前,一轮之后最小数就在最前面了
temp = arr[j - 1];
arr[j - 1] = arr[j];
arr[j] = temp;
}
}
}
}
二分法
public static int myBinarySearch(int[] arr,int value) {
int low=0;
int high=arr.length-1;
while(low<=high) {
int mid=(low+high)/2;
if(value==arr[mid]) {
return mid;
}
if(value>arr[mid]) {
low=mid+1;
}
if(value<arr[mid]) {
high=mid-1;
}
}
return -1;//没有找到返回-1
}