├── Android
└── Android.md
├── Answer.md
├── JVM
└── JVM 面试题集.md
├── README.md
├── 操作系统
├── 中断与系统调用.md
├── 什么是死锁,死锁产生的条件以及处理方法.md
├── 什么是虚拟内存.md
├── 内存管理中分页和分段有什么区别?.md
├── 操作系统面试题集.md
├── 缓冲区溢出.md
├── 进程同步的几种机制.md
├── 进程和线程的区别.md
├── 进程有哪几种状态.md
├── 进程调度策略.md
└── 进程间通信的几种方式.md
└── 计算机网络
└── HTTPS.md
/Android/Android.md:
--------------------------------------------------------------------------------
1 | ---
2 | 试题采集
3 | ---
4 |
5 | #### 06-14
6 |
7 | https://www.nowcoder.com/discuss?type=2&order=0&pageSize=30&expTag=4&page=4
8 |
9 | #### Android
10 |
11 | 1. AsyncTask
12 | 2. Handler
13 | 3. View 绘制流程
14 | 4. adb
15 | 5. RecyclerView 源码,对比 ListView 有哪些改进?
16 | 6. 主线程为什么不会因为 Looper 而阻塞?
17 | 7. 事件分发机制、滑动冲突处理
18 | 8. 自定义 View,实现下拉刷新控件?
19 | 9. 如何计算 Bitmap 大小?Bitmap 压缩。
20 | 10. 有一个很大很大的图片加载到内存上,不能降低清晰度和压缩图片你怎么解决?(提示我局部显示?我没懂)
21 | 11. 设计一个图片浏览框架,(线程池,lru缓存,brabra的说了一堆)
22 | 12. Fragment 懒加载
23 | 13. APP从打开到显示之间发生的事情
24 | 14. 热修复与插件化
25 | 15. 断点续传
26 | 16. AMS、Binder
27 | 17. 动态权限
28 | 18. JNI 开发中有碰到什么问题?怎么定位 Native Crash
29 | 19. 屏幕刷新机制,怎么优化 UI 卡顿情况
30 | 20. 插件化框架
31 | 21. Android 源码中的设计模式
32 | 22. 一个 Android 正常启动有多少个线程?
33 | 23. ART 和 DVK 在 GC 上有啥不同?有啥改进?
34 | 24. Activity 启动流程?
35 | 25. 多渠道打包
36 | 26. 跨进程通信
37 |
38 | #### 操作系统
39 |
40 |
41 |
42 | #### 算法
43 |
44 | 1. 两个线程交替打印 0-100
45 | 2. 给定一个数,如何在一个有序列表找到两个数的和等于这个数?
46 | 3. 动态规划
47 | 4. 大数相乘
48 | 5. 两个栈实现队列,两个队列实现栈
49 | 6. 判断二叉树是否是镜像、二叉树深度、二叉树左右子树交换、最大路径、层次遍历、之字遍历
50 | 7. 数组找第 K 大数
51 | 8. 1-1000 有多少个 1
52 | 9. 一个数组中,找到出现次数最多的元素并输出
53 | 10. 链表的中间元素并输出
54 | 11. 翻转数字 给一个int值 返回它的相反数 例如 123 -> 321(正负情况,超出int范围抛出什么异常)
55 | 12. 判断一个8bit串只含有4个1,要求比较次数小于遍历
56 | 13. 递归、非递归翻转链表
57 | 14. 打印回环数组
58 | 15. 找到一个无序数组中第一次出现最多次数最多的元素
59 | 16. 快排思想、时间复杂度,什么场景对应着最坏的情况
60 | 17. 给定无序数组和一个值,找到两个数和为值得元素,不能使用额外的空间复杂度
61 | 18. k 个有序链表合并
62 | 19. 一个文件有一亿个字符串,统计频率最高的十个
63 | 20. 红黑树和平衡二叉树区别?查找复杂度?哪个插入效率高?
64 | 21. 给出两个链表的头指针比如p1,p2,判断这两个链表是否相交
65 | 22. 二维数组,每一行,每一列都是升序,找出某数的下标,没有输出[-1,-1],最好的时间复杂度是m+n(行数+列数)
66 | 23. 俩线程分别持续打印奇数和偶数,实现俩线程的交替打印(从小到大)
67 | 24. 1~10000中7出现的次数,如17算1个,77算2个
68 |
69 | #### 计算机网路
70 |
71 | 1. HTTP 和 HTTPS 的区别?SSL 握手过程?
72 | 2. TCP、UDP
73 | 3. TCP / IP 四层模型,网络层和传输层有什么区别?网络层的主要工作是什么?
74 | 4. Socket.accept() 函数对应着 TCP 三次握手中哪一次握手,哪个函数代表连接建立完毕,四次挥手的过程?
75 | 5. HTTPS 实现思路
76 | 6. OSI 模型
77 |
78 | #### Java
79 |
80 | 1. 线程和进程的区别、线程有哪些状态、创建线程有哪几种方法、调用哪些方法会阻塞线程?
81 | 2. 反射机制与原理
82 | 3. 四种引用
83 | 4. Object 的方法有哪些?
84 | 5. hash 算法的原理,HashMap 源码
85 | 6. 用四个线程计算数组和
86 | 7.
87 |
88 | #### JVM
89 |
90 | 1. GC 回收
91 | 2. 双亲委派机制
92 | 3. 形成死锁的四个条件
93 |
94 | #### 设计模式
95 |
96 |
--------------------------------------------------------------------------------
/Answer.md:
--------------------------------------------------------------------------------
1 | # Android_QA
2 |
3 | ## Java 部分(一)基础知识点
4 |
5 | ### 1. Java中 == 和 equals 和 hashCode 的区别
6 |
7 | Java中的数据类型可分为两类,基本数据类型和引用类型。基本数据类型:byte、short、char、int、long、float、double、boolean。他们之间的比较用双等号(==),比较的是**值**。引用类型:类、接口、数组。当他们用双等号(==)进行比较的时候,比较的是他们在**内存中的存放地址**。对象是放在堆中的,栈中存放的是对象的引用(地址)。由此可见,**双等号是对栈中的值进行比较的**。如果要比较堆中对象是否相同,那么就要重写equals方法了。
8 |
9 | 默认情况下(没有覆盖equals方法)下的equals方法都是调用Object类的equals方法,而Object的equals方法主要是用于判断**对象的内存地址引用是不是同一个地址**(是不是同一个对象)。下面是Object类中的equals方法:
10 |
11 | ```java
12 | public boolean equals(Object obj) {
13 | return (this == obj);
14 | }
15 | ```
16 |
17 | 定义的equals方法与==是等效的。
18 |
19 | **但是**,要是类中覆盖了equals方法,那么就要根据具体代码来确定equals方法的作用了。**覆盖后的一般都是通过对象的内容是否相等来判断对象是否相等。**下面是String类对equals方法进行了重写:
20 |
21 | ```java
22 | public boolean equals(Object anObject) {
23 | if (this == anObject) {
24 | return true;
25 | }
26 | if (anObject instanceof String) {
27 | String anotherString = (String)anObject;
28 | int n = count;
29 | if (n == anotherString.count) {
30 | char v1[] = value;
31 | char v2[] = anotherString.value;
32 | int i = offset;
33 | int j = anotherString.offset;
34 | while (n-- != 0) {
35 | if (v1[i++] != v2[j++])
36 | return false;
37 | }
38 | return true;
39 | }
40 | }
41 | return false;
42 | }
43 | ```
44 |
45 | 在 Object#equals 方法注释上,也给出了重写 equals 函数要遵守的规则:自反性、对称性、传递性和一致性,并且给出了具体示例。注释中还给出了,重写 equals 方法时也要重写 hashCode 方法,从而维持 hashCode 的语义,即如果对象相等,那么他们的哈希值必须相同。
46 |
47 | hashCode()方法返回的就是一个数值,从方法名上来看,其目的就是生成一个hash码,hash码的主要用途就是在**对对象进行散列的时候作为key输入**。
48 |
49 | 参考自:[http://blog.csdn.net/hla199106/article/details/46907725](http://blog.csdn.net/hla199106/article/details/46907725)
50 |
51 |
52 |
53 | ### 2. int、char、long 各占多少字节数
54 |
55 | | 类型 | 位数 | 字节数 |
56 | | ------ | ---- | ---- |
57 | | boolen | 8 | 1 |
58 | | int | 32 | 4 |
59 | | float | 32 | 4 |
60 | | double | 64 | 8 |
61 | | char | 16 | 2 |
62 | | byte | 8 | 1 |
63 | | short | 16 | 2 |
64 | | long | 64 | 8 |
65 |
66 |
67 |
68 | ### 3. int 和 Integer 的区别
69 |
70 | Java 为每一个基本数据类型都引入了对应的包装类,从Java 5 开始引入了自动装箱 / 拆箱机制,使得两者可以相互转化。**所以最基本的一点区别就是**:Integer 是int的包装类,int的初始值为零,而Integer的初值为null。int与Integer相比,会把Integer自动拆箱为int再去比。
71 |
72 | 参考自:[http://blog.csdn.net/login_sonata/article/details/71001851](http://blog.csdn.net/login_sonata/article/details/71001851)
73 |
74 |
75 |
76 | ### 4. 谈谈对Java多态的理解
77 |
78 | 多态即:事物在运行过程中存在不同的状态。多态的存在有三个前提:要有继承关系、子类重写父类方法、父类数据类型的引用指向子类对象。弊端就是:不能使用子类特有的成员属性和子类特有的成员方法。如果非要用到不可,可以强制类型转换。
79 |
80 | 参考自:[https://item.btime.com/m_2s22uri6z16](https://item.btime.com/m_2s22uri6z16)
81 |
82 |
83 |
84 | ### 5. String、StringBuffer、StringBuilder的区别
85 |
86 | String:字符串常量,使用字符串拼接时是不同的两个空间。
87 |
88 | StringBuffer:字符串变量,线程安全,字符串拼接直接在字符串后追加。
89 |
90 | StringBuilder:字符串变量,非线程安全,字符串拼接直接在字符串后追加。
91 |
92 | StringBuilder执行效率高于StringBuffer高于String。String是一个常量,是不可变的,所以对于每次+=赋值都会创建一个新的对象,StringBuilder和StringBuffer都是可变的,当进行字符串拼接的时候采用append方法,在原来的基础上追加,所以性能要比String高,因为StringBuffer是线程安全的而StringBuilder是线程非安全的,所以StringBuilder的效率高于StringBuffer。对于大数据量的字符串拼接,采用StringBuffer、StringBuilder。另一种说法,JDK1.6做了优化,通过String声明的字符串在进行用+进行拼接时,底层调用的是StringBuilder,所以性能基本上和后两者没什么区别。
93 |
94 |
95 |
96 | ### 6. 什么是内部类?内部类的作用
97 |
98 | Java 常见的内部类有四种:成员内部类、静态内部类、方法内部类和匿名内部类。
99 |
100 | 内部类可以很好的实现隐蔽,一般的非内部类,是不允许有private和protectd等权限的,但内部类(除方法内部类)可以通过这些修饰符来实现隐蔽。
101 |
102 | 内部类拥有外部类的访问权限(分静态与非静态情况),通过这一特性可以比较好的处理类之间的关联性,将一类事物的流程放在一起内部处理。
103 |
104 | 通过内部类可以实现多重继承,Java默认是单继承的,我们可以通过多个内部类继承实现多个父类,接着由于外部类完全可访问内部类,所以就实现了类似多继承的效果。
105 |
106 | 参考自:[https://www.jianshu.com/p/7fe3af7f0f2d](https://www.jianshu.com/p/7fe3af7f0f2d)
107 |
108 |
109 |
110 | ### 7. 抽象类和接口的区别
111 |
112 | 抽象类使用abstract class 定义,抽象类既可以有抽象方法也可以有其他类型的方法,既可以有静态属性也可以有非静态属性。
113 |
114 | 接口使用interface定义,属性用public final static 修饰,如果没有写关键字,虚拟机默认会加上关键字。JDK8之后接口里的方法既可以有抽象方法也可以有默认方法。
115 |
116 | 抽象类是一种类别,具有构造方法。接口是一种行为规范,没有构造方法。抽象类中可以没有抽象方法,他有自己的构造方法但是不能直接实例化对象,所以必须被子类继承之后才有意义。
117 |
118 | 抽象类只能单继承,接口可以被多个类实现,接口和接口之间可以多继承。
119 |
120 | 抽象类可以由默认的方法实现,接口根本不存在方法的实现。实现抽象类使用extends关键字来继承抽象类,如果子类不是抽象类,它需要提供抽象类中所有抽象方法的实现。子类使用关键字implements来实现接口,它需要提供接口中所有声明的方法的实现。
121 |
122 | 抽象方法可以有public、protectd 和 default 这些修饰符,而接口方法默认修饰符是public,不可以使用其他修饰符。
123 |
124 | 抽象类比接口速度快,接口是稍微有点慢的,这是因为它需要时间去寻找类中实现的方法。
125 |
126 | 参考自:[http://www.importnew.com/12399.html](http://www.importnew.com/12399.html)
127 |
128 |
129 |
130 | ### 8. 抽象类的意义
131 |
132 | 为子类提供一个公共的类型;封装子类中重复的内容;定义有抽象方法,子类虽然有不同的实现,但该方法的定义是一致的;
133 |
134 |
135 |
136 | ### 9. 抽象类与接口的应用场景
137 |
138 | 如果你拥有一些方法并且想让它们中的一些默认实现,那么就用抽象类;如果你想实现多继承,那么必须实用接口;如果基本功能在不断变化,那么就需要使用抽象类,如果不断改变基本功能并且使用接口,那么就要改变所有实现了该接口的类。
139 |
140 |
141 |
142 | ### 10. 抽象类是否可以没有方法和属性?
143 |
144 | 可以,抽象类可以没有方法和属性,但是含有抽象方法的类一定是抽象类。
145 |
146 |
147 |
148 | ### 11. 接口的意义
149 |
150 | 规范、扩展、回调。
151 |
152 | 对一些类是否具有某个功能非常关心,但不关心功能的具体实现,那么就需要这些类实现一个接口。
153 |
154 |
155 |
156 | ### 12. 泛型中的extends和super的区别
157 |
158 | ```
159 | extends T>和 super T>是泛型中的“通配符”和“边界”的概念。
160 | extends T>:是指“上界通配符”,不能往里存,只能往外取。
161 | super T>:是指“下界通配符”,不影响往里存,但往外取只能放在Object对象里。
162 | ```
163 |
164 | **PECS原则:频繁往外读取内容,适合用上界Extends,经常往里插入的,适合用下界Super。**
165 |
166 | 参考自:[[Java]泛型中 extends 和 super 的区别?](https://itimetraveler.github.io/2016/12/27/%E3%80%90Java%E3%80%91%E6%B3%9B%E5%9E%8B%E4%B8%AD%20extends%20%E5%92%8C%20super%20%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F/)
167 |
168 |
169 |
170 | ### 13. 父类的静态方法能否被子类重写?
171 |
172 | 不能,子类继承父类后,非静态方法覆盖父类的方法,父类的静态方法被隐藏。
173 |
174 |
175 |
176 | ### 14. 进程和线程的区别
177 |
178 | 进程是资源分配的基本单位,线程是处理器调度的基本单位。
179 |
180 | 进程拥有独立的地址空间,线程没有独立的地址空间,同一个进程内多个线程共享其资源。
181 |
182 | 线程划分尺度更小,所以多线程程序并发性更高。
183 |
184 | 一个程序至少有一个进程,一个进程至少有一个线程。
185 |
186 | 线程是属于进程的,当进程退出时该进程所产生的线程都会被强制退出并清除。
187 |
188 | 线程占用的资源要少于进程所占用的资源。
189 |
190 |
191 |
192 | ### 15. final、finally、finalize的区别
193 |
194 | final用于声明属性、方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。
195 |
196 | finally是异常处理语句结构的一部分,表示总是执行。
197 |
198 | finalize是Object类的一个方法,在垃圾收集器执行的时候会回调被回收对象的finalize()方法,可以覆盖此方法提供垃圾收集时其他资源的回收,例如关闭文件。
199 |
200 |
201 |
202 | ### 16. 序列化的方式
203 |
204 | 实现Serializable接口和实现Parcelable接口。
205 |
206 |
207 |
208 | ### 17. Serializable 和 Parcelable 的区别
209 |
210 | Serializable 是 Java的序列化接口。特点是简单,直接实现该接口就行了,其他工作都被系统完成了,但是对于内存开销大,序列化和反序列化需要很多的 I/O 流操作。
211 |
212 | Parcelable 是Android的序列化方式,主要用于在内存序列化上,采用该方法需要实现该接口并且手动实现序列化过程,相对复杂点。如果序列化到存储设备(文件等)或者网络传输,比较复杂,建议用Serializable 方式。
213 |
214 | **最大的区别就是存储媒介的不同:**Serializable 使用IO读写存储在硬盘上,Parcelable 是直接在内存中读写,内存读写速度远大于IO读写,所以Parcelable 更加高效。Serializable在序列化过程中会产生大量的临时变量,会引起频繁的GC,效率低下。
215 |
216 |
217 |
218 | ### 18. 静态属性和静态方法是否可以被继承?是否可以被重写?以及原因。
219 |
220 | 静态属性和静态方法可以被继承,但是没有被重写而是被隐藏。
221 |
222 | 静态方法和属性是属于类的,调用的时候直接通过 类名.方法名 完成,不需要继承机制就可以调用。如果子类里面定义了静态方法和属性,那么这时候父类的静态方法或属性称之为“隐藏”。至于是否继承一说,子类是有继承静态方法和属性,但是跟实例方法和属性不太一样,存在隐藏这种情况。
223 |
224 | 多态之所以能够实现依赖于继承、接口和重写、重载(继承和重写最为关键)。**有了继承和重写就可以实现父类的引用指向不同子类的对象。** 重写的功能是:重写后子类的优先级高于父类的优先级,但是隐藏是没有这个优先级之分的。
225 |
226 | 静态属性、静态方法和非静态的属性都可以被继承和隐藏而不能被重写,因此不能实现多态,不能实现父类的引用可以指向不同子类的对象。
227 |
228 | 非静态方法可以被继承和重写,因此可以实现多态。
229 |
230 |
231 |
232 | ### 19. 静态内部类的设计意图
233 |
234 | 只是为了降低包的深度,方便类的使用,静态内部类适用于包含类当中,但又不依赖于外在的类,不用使用外在类的非静态属性和方法,只是为了方便管理类结构而定义。在创建静态内部类的时候,不需要外部类对象的引用。
235 |
236 | 非静态内部类有一个很大的优点:可以自由使用外部类中的变量和方法。
237 |
238 | ```java
239 | static class Outer {
240 | class Inner {}
241 | static class StaticInner {}
242 | }
243 |
244 | Outer outer = new Outer();
245 | Outer.Inner inner = outer.new Inner();
246 | Outer.StaticInner inner0 = new Outer.StaticInner();
247 | ```
248 |
249 | 参考自:[https://www.zhihu.com/question/28197253](https://www.zhihu.com/question/28197253)
250 |
251 |
252 |
253 | ### 20. 成员内部类、静态内部类、方法内部类(局部内部类)和匿名内部类的理解,以及项目中的应用
254 |
255 | 成员内部类:
256 |
257 | 最普通的内部类,它的定义位于一个类的内部,这样看起来,成员内部类相当于外部类的一个成员,成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。不过需要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,形式如下:
258 |
259 | ```java
260 | 外部类.this.成员变量
261 | 外部类.this.成员方法
262 | ```
263 |
264 | 虽然成员内部类可以无条件的访问外部类的成员,而外部类想访问成员内部类的成员却不是那么随心所欲了。成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提必须存在一个外部类对象。创建成员内部类对象的一般方式如下:
265 |
266 | ```java
267 | public class Test {
268 | public static void main(String[] args) {
269 | //第一种方式:
270 | Outter outter = new Outter();
271 | Outter.Inner inner = outter.new Inner(); //必须通过Outter对象来创建
272 |
273 | //第二种方式:
274 | Outter.Inner inner1 = outter.getInnerInstance();
275 | }
276 | }
277 |
278 | class Outter {
279 | private Inner inner = null;
280 | public Outter() {
281 |
282 | }
283 |
284 | public Inner getInnerInstance() {
285 | if(inner == null)
286 | inner = new Inner();
287 | return inner;
288 | }
289 |
290 | class Inner {
291 | public Inner() {
292 |
293 | }
294 | }
295 | }
296 | ```
297 |
298 | 内部类可以拥有private、protected、public以及包访问权限。如果成员内部类用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问,如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问,如果是默认访问权限,则只能是同一个包下访问。这一点和外部类有一点不一样,外部类只能被public和包访问两种权限修饰。由于成员内部类看起来像外部类的一个成员,所以可以像类的成员一样拥有多种修饰权限。
299 |
300 | 静态内部类:
301 |
302 | 静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖外部类的,这点和类的静态成员属性有点相似,并且它不能使用外部类的非static成员变量或者方法。这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依赖于具体的对象。
303 |
304 | 局部内部类:
305 |
306 | 局部内部类是定义在一个方法或者一个作用域里面的的类,它和成员内部类的区别在于局部内部类的访问权限仅限于方法内或者该作用域内。注意,局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。
307 |
308 | 匿名内部类:
309 |
310 | 匿名内部类不能有访问修饰符和static修饰符的,一般用于编写监听代码。匿名内部类是唯一一个没有构造器的类。正因为没有构造器,所以匿名内部类的适用范围非常有限,大部分匿名内部类用于接口回调。一般来说,匿名内部类用于继承其他类或者实现接口,并不需要增加额外的方法,只是对继承方法的实现或者是重写。
311 |
312 | 应用场景:
313 |
314 | 1. 最重要的一点,每个内部类都能独立的继承一个接口的实现,无论外部类是否已经继承了某个接口的实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整。
315 |
316 | 2. 方便将存在一定逻辑关系的类组织在一起,又可以对外界隐蔽。
317 | 3. 方便编写事件驱动程序
318 | 4. 方便编写线程代码
319 |
320 | 总结:
321 |
322 | 对于成员内部类,必须先产生外部类的实例化对象,才能产生内部类的实例化对象。而非静态内部类不用产生外部类的实例化对象即可产生内部类的实例化对象。
323 |
324 | ```java
325 | 创建静态内部类对象的一般形式:
326 | 外部类类名.内部类类名 xxx = new 外部类类名.内部类类名()
327 | 创建成员内部类对象的一般形式:
328 | 外部类类名.内部类类名 xxx = new 外部类对象名.new 内部类类名()
329 | ```
330 |
331 | 参考自:[http://www.cnblogs.com/latter/p/5665015.html](http://www.cnblogs.com/latter/p/5665015.html)
332 |
333 |
334 |
335 | ### 21. 谈谈对kotlin的理解
336 |
337 | 挖个坑,以后自己填!哼哼(傲娇脸
338 |
339 |
340 |
341 | ### 22. 闭包和局部内部类的区别
342 |
343 |
344 |
345 | ### 23. String转换成Integer的方式以及原理
346 |
347 | String转int:
348 |
349 | ```java
350 | int i = Integer.parseInt(s);
351 | int i = Integer.valueOf(s).intValue();
352 | ```
353 |
354 | int转String:
355 |
356 | ```java
357 | String s = i + "";
358 | String s = Integer.toString(i);
359 | String s = String.valueOf(i);
360 | ```
361 |
362 | 源码:
363 |
364 | ```java
365 | public static int parseInt(String s, int radix) throws NumberFormatException
366 | {
367 | /*
368 | * WARNING: This method may be invoked early during VM initialization
369 | * before IntegerCache is initialized. Care must be taken to not use
370 | * the valueOf method.
371 | */
372 | // 下面三个判断好理解,其中表示进制的 radix 要在(2~36)范围内
373 | if (s == null) {
374 | throw new NumberFormatException("null");
375 | }
376 |
377 | if (radix < Character.MIN_RADIX) {
378 | throw new NumberFormatException("radix " + radix +
379 | " less than Character.MIN_RADIX");
380 | }
381 |
382 | if (radix > Character.MAX_RADIX) {
383 | throw new NumberFormatException("radix " + radix +
384 | " greater than Character.MAX_RADIX");
385 | }
386 |
387 | int result = 0; // 表示结果, 在下面的计算中会一直是个负数,
388 | // 假如说 我们的字符串是一个正数 "7" ,
389 | // 那么在返回这个值之前result保存的是 -7,
390 | // 这个可能是为了保持正数和负数在下面计算的一致性
391 | boolean negative = false;
392 | int i = 0, len = s.length();
393 |
394 |
395 | //limit 默认初始化为 最大正整数的 负数 ,假如字符串表示的是正数,
396 | //那么result(在返回之前一直是负数形式)就必须和这个最大正数的负数来比较,判断是否溢出
397 |
398 | int limit = -Integer.MAX_VALUE;
399 |
400 | int multmin;
401 | int digit;
402 |
403 | if (len > 0) { // 首先是对第一个位置判断,是否含有正负号
404 | char firstChar = s.charAt(0);
405 | if (firstChar < '0') { // Possible leading "+" or "-"
406 | if (firstChar == '-') {
407 | negative = true;
408 |
409 | // 这里,在负号的情况下,判断溢出的值就变成了 整数的 最小负数了。
410 | limit = Integer.MIN_VALUE;
411 |
412 | } else if (firstChar != '+')
413 | throw NumberFormatException.forInputString(s);
414 |
415 | if (len == 1) // Cannot have lone "+" or "-"
416 | throw NumberFormatException.forInputString(s);
417 | i++;
418 | }
419 |
420 | multmin = limit / radix;
421 | // 这个是用来判断当前的 result 在接受下一个字符串位置的数字后会不会溢出。
422 | // 原理很简单,为了方便,拿正数来说
423 | // (multmin result 在计算中都是负数),假如是10
424 | // 进制,假设最大的10进制数是 21,那么multmin = 21/10 = 2,
425 | // 如果我此时的 result 是 3 ,下一个字符c来了,result即将变成
426 | // result = result * 10 + c;那么这个值是肯定大于 21 ,即溢出了,
427 | // 这个溢出的值在 int里面是保存不了的,不可能先计算出
428 | // result(此时的result已经不是溢出的那个值了) 后再去与最大值比较。
429 | // 所以要通过先比较 result < multmin (说明result * radix 后还比 limit 小)
430 |
431 | while (i < len) {
432 | // Accumulating negatively avoids surprises near MAX_VALUE
433 | digit = Character.digit(s.charAt(i++),radix);
434 | if (digit < 0) {
435 | throw NumberFormatException.forInputString(s);
436 | }
437 |
438 |
439 | // 这里就是上说的判断溢出,由于result统一用负值来计算,所以用了 小于 号
440 | // 从正数的角度看就是 reslut > mulmin 下一步reslut * radix 肯定是 溢出了
441 | if (result < multmin) {
442 | throw NumberFormatException.forInputString(s);
443 | }
444 |
445 |
446 |
447 | result *= radix;
448 |
449 | // 这里也是判断溢出, 由于是负值来判断,相当于 (-result + digit)> - limit
450 | // 但是不能用这种形式,如果这样去比较,那么得出的值是肯定判断不出溢出的。
451 | // 所以用 result < limit + digit 很巧妙
452 | if (result < limit + digit) {
453 |
454 | throw NumberFormatException.forInputString(s);
455 | }
456 | result -= digit; // 加上这个 digit 的值 (这里减就相当于加)
457 | }
458 | } else {
459 | throw NumberFormatException.forInputString(s);
460 | }
461 | return negative ? result : -result; // 正数就返回 -result
462 | }
463 | ```
464 |
465 | 参考自:[http://blog.csdn.net/stu_wanghui/article/details/38564177](http://blog.csdn.net/stu_wanghui/article/details/38564177)
466 |
467 |
468 |
469 | ### 24. 面向对象思想
470 |
471 | #### 设计原则 S.O.L.I.D
472 |
473 | | 简写 | 翻译 |
474 | | ---- | ------ |
475 | | SRP | 单一职责原则 |
476 | | OCP | 开放封闭原则 |
477 | | LSP | 里氏替换原则 |
478 | | ISP | 接口分离原则 |
479 | | DIP | 依赖倒置原则 |
480 |
481 | **单一职责原则**
482 |
483 | > 修改一个类的原因应该只有一个
484 |
485 | 换句话说就是让一个类只负责一件事,当这个类需要做过多的事情的时候,就需要分解这个类。
486 |
487 | 如果一个类承担的职责过多,就等于把这些职责耦合在了一起,一个职责的变化可能会削弱这个类完成其它职责的能力。
488 |
489 | **开放封闭原则**
490 |
491 | > 类应该对扩展开放,对修改关闭
492 |
493 | 扩展就是添加新功能的意思,因此该原则要求在添加新功能时不需要修改代码。
494 |
495 | 符合开闭原则最典型的设计模式是装饰者模式,它可以动态地将职责附加到对象上,而不用去修改类的代码。
496 |
497 | **里氏替换原则**
498 |
499 | > 子类对象必须能够替换掉所有父类对象
500 |
501 | 继承是一种 IS - A 关系,子类需要能够当成父类来使用,并且需要比父类更特殊。
502 |
503 | 如果不满足这个原则,那么各个子类的行为上就会有很大差异,增加继承体系的复杂度。
504 |
505 | **接口分离原则**
506 |
507 | > 不应该强迫客户依赖于它们不用的方法
508 |
509 | 因此使用多个专门的接口比使用单一的总接口更好。
510 |
511 | **依赖倒置原则**
512 |
513 | > 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
514 | >
515 | > 抽象不应该依赖于细节,细节应该依赖于抽象。
516 |
517 | 高层模块包含一个应用程序中重要的策略选择和业务模块,如果高层模块依赖于低层模块,那么低层模块的改动就会直接影响到高层模块,从而迫使高层模块也需要改动。
518 |
519 | 依赖于抽象意味着:
520 |
521 | * 任何变量都不应该持有一个指向具体类的指针或者引用
522 | * 任何类都不应该从具体类派生
523 | * 任何方法都不应该覆写它的任何基类中的已经实现的方法
524 |
525 | #### 三大特性
526 |
527 | **封装**
528 |
529 | 利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分离的独立实体。数据被保护在抽象数据类型的内部,尽可能的隐藏内部细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
530 |
531 | 优点:
532 |
533 | * 降低耦合:可以独立开发、测试、优化和修改等
534 | * 减轻维护的负担:可以更容易的被程序猿理解,并且在调试的时候可以不影响其他模块
535 | * 有效地调节性能:可以通过剖析确定哪些模块影响了系统的性能
536 | * 提高软件的可重用性
537 | * 降低了构建大型系统的分享:即使整个系统不可用,但是这些独立的模块却有可能是可用的
538 |
539 | **继承**
540 |
541 | 继承实现了 IS - A 关系,继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。
542 |
543 | **多态**
544 |
545 | 多态分为编译时多态和运行时多态。编译时多态主要指方法的重载,运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定。
546 |
547 | 运行时多态有三个条件:
548 |
549 | * 继承
550 | * 覆盖
551 | * 向上转型
552 |
553 | ### 25. 对象拷贝理解?深拷贝、浅拷贝的区别?
554 |
555 | 首先要先明白为什么需要使用克隆呢?
556 |
557 | 克隆的对象可能包含一些已经修改过的属性,而 new 出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的 "状态" 就需要克隆了。
558 |
559 | 那如何实现对象克隆呢?有两种办法:
560 |
561 | 1. 实现 Cloneable 接口并重写 Object 类中的 clone() 方法
562 | 2. 实现 Serialiable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆
563 |
564 | 深拷贝和浅拷贝的区别是什么?
565 |
566 | - 浅拷贝
567 |
568 | 当对象被复制的时候只复制它本身和其中包含的值类型的成员变量,而引用类型的成员变量并没有复制。
569 |
570 | - 深拷贝
571 |
572 | 除了对象本身被复制外,对象所包含的所有成员变量也将被复制。
573 |
574 | Java 默认的是浅拷贝,如果想实现深拷贝,就需要对象所包含的引用类型的成员变量也需要实现 Cloneable 接口,或者实现 Serialiable 接口。
575 |
576 | ### 26. Enumeration 和 Iterator 的区别?
577 |
578 | - 接口不同
579 |
580 | Enumeration 只能读取集合数据,而不能对数据进行修改;Iterator 除了读取集合的数据之外,也能对数据进行删除操作;Enumeration 已经被 Iterator 取代了,之所以没有被标记为 Deprecated,是因为在一些遗留类(Vector、Hashtable)中还在使用。
581 |
582 | ```java
583 | public interface Enumeration {
584 | boolean hasMoreElements();
585 | E nextElement();
586 | default Iterator asIterator() {
587 | return new Iterator<>() {
588 | @Override public boolean hasNext() {
589 | return hasMoreElements();
590 | }
591 | @Override public E next() {
592 | return nextElement();
593 | }
594 | };
595 | }
596 | }
597 | ```
598 |
599 | ```java
600 | public interface Iterator {
601 | boolean hasNext();
602 | E next();
603 | default void remove() {
604 | throw new UnsupportedOperationException("remove");
605 | }
606 | default void forEachRemaining(Consumer super E> action) {
607 | Objects.requireNonNull(action);
608 | while (hasNext())
609 | action.accept(next());
610 | }
611 | }
612 | ```
613 |
614 | - Iterator 支持 fail-fast 机制,而 Enumeration 不支持
615 |
616 | Enumeration 是 JDK 1.0 添加的接口。使用到它的函数包括 Vector、Hashtable 等类,Enumeration 存在的目的就是为它们提供遍历接口。
617 |
618 | Iterator 是 JDK 1.2 才添加的接口,它是为了 HashMap、ArrayList 等集合提供的遍历接口。Iterator 是支持 fail-fast 机制的。
619 |
620 | Fail-fast 机制是指 Java 集合 (Collection) 中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。例如:当某个线程 A 通过 Iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变了,那么线程 A 访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事件。
621 |
622 | ## Java 部分(二)高级知识点
623 |
624 | ### 1. 哪些情况下的对象会被垃圾回收机制处理掉?
625 |
626 | 垃圾回收机制中最基本的做法是分代回收。内存区域被划分为三代:新生代、老年代和永久代。对于不同世代可以使用不同的垃圾回收算法。一般来说,一个应用中的大部分对象的存活时间都很短,基于这一点,对于新生代的垃圾回收算法就可以很有针对性。
627 |
628 | 参考自:[Java — 垃圾收集灵魂拷问三连(一)](http://omooo.top/2017/12/25/Java%20---%20%E5%85%B3%E4%BA%8E%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E7%81%B5%E9%AD%82%E6%8B%B7%E9%97%AE%E4%B8%89%E8%BF%9E%EF%BC%88%E4%B8%80%EF%BC%89)
629 |
630 | ### 2. 讲一下常见的编码方式?
631 |
632 | 编码的原因:
633 |
634 | 计算机存储信息的最小单元是一个字节即八个bit,所以能表示的字符范围是0 - 225 个。然而要表示的符号太多了,无法用一个字节来完全表示,要解决这个矛盾必须需要一个新的数据结构char,从char到byte必须编码。编码方式规定了转化的规则,按照这个规则就可以让计算机正确的表示我们的字符。编码方式有以下几种:
635 |
636 | ASCII码:
637 |
638 | 总共有128个,用一个字节的低七位表示,0 - 31是控制字符,如回车键、换行等等。32 - 126是打印字符,可以通过键盘输入并且能够显示出来。
639 |
640 | ISO-8859-1:
641 |
642 | ISO组织在ASCII码基础上又制定了一些列标准用来扩展ASCII编码,其中IS-8859-1涵盖了大多数西欧语言字符,单字节编码,总共表示256个字节,应用广泛。
643 |
644 | GB2312、GBK:
645 |
646 | 双字节编码,表示汉字。
647 |
648 | UTF-16:
649 |
650 | UTF - 16具体定义了Unicode字符在计算机中的存取方法。UTF - 16用两个字节来表示Unicode转化格式,这个是定长的表示方法,不论什么字符都可以用两个字节表示,两个字节是十六个bit,所以叫UTF - 16.
651 |
652 | UTF- 8:
653 |
654 | UTF-16统一采用两个字节表示一个字符,虽然表示上非常简单方便,但是有很大一部分字符用一个字节就可以表示,显然是浪费了存储空间。于是UTF-8应运而生,UTF-8采用一种变长技术,每个编码区域有不同的字码长度,不同类型的字符可以是由1-6个字节组成。
655 |
656 | 参考自:[几种常见的编码格式](https://www.cnblogs.com/mlan/p/7823375.html)
657 |
658 |
659 |
660 | ### 3. UTF-8编码中中文占几个字节,int型几个字节?
661 |
662 | 中文占字节数可以是2、3和4个的,最高为4个字节。
663 |
664 | 参考自:[http://blog.csdn.net/hellokatewj/article/details/24325653](http://blog.csdn.net/hellokatewj/article/details/24325653)
665 |
666 |
667 |
668 | ### 4. 静态代理和动态代理的区别,什么场景使用?
669 |
670 | 参考自:[https://www.jianshu.com/p/2f518a4a4c2b](https://www.jianshu.com/p/2f518a4a4c2b)
671 |
672 |
673 |
674 | ### 5. Java的异常体系
675 |
676 | 异常简介:异常是阻止当前方法继续执行的问题,如:文件找不到、网络连接失败、非法参数等等。发现异常的理想时期是编译阶段,然而编译阶段不能找出所有的异常,余下的问题必须在运行期间。
677 |
678 | Java中的异常分为可查异常和不可查异常。
679 |
680 | **可查异常:**
681 |
682 | 即编译时异常,只编译器在编译时可以发现的错误,程序在运行时很容易出现的异常状况,这些异常可以预计,所以在编译阶段就必须手动进行捕捉处理,即要用try-catch语句捕获它,要么用throws子句声明抛出,否者编译无法通过,如:IOException、SQLException以及用户自定义Exception异常。
683 |
684 | **不可查异常:**
685 |
686 | 不可查异常包括**运行时异常**和**错误**,他们都是在程序运行时出现的。异常和错误的区别:异常能被程序本身处理,错误是无法处理的。**运行时异常指的是**程序在运行时才会出现的错误,由程序员自己分析代码决定是否用try...catch进行捕获处理。如空指针异常、类转换异常、数组下标越界异常等等。**错误是指**程序无法处理的错误,表示运行应用程序中较严重的问题,如系统崩溃、虚拟机错误、动态连接失败等等。大多数错误与代码编写者执行的操作无关,而表示代码运行时JVM出现的问题。
687 |
688 | **Java异常类层次结构图:**
689 |
690 | 
691 |
692 | 从上图可以看出Java通过API中Throwable类的众多子类描述各种不同的异常。因而,Java异常都是对象,是Throwable子类的实例。
693 |
694 | **异常处理机制:**
695 |
696 | 当异常发生时,将使用new在堆上创建一个异常对象,对于这个异常对象,有两种处理方式:
697 |
698 | 1. 使用throw关键字将异常对象抛出,则当前执行路径被终止,异常处理机制将在其他地方寻找catch快对异常进行处理
699 | 2. 使用try...catch在当前逻辑中进行捕获处理
700 |
701 | **throws 和 throw:**
702 |
703 | throws:一个方法在声明时可以使用throws关键字声明可能会产生的若干异常
704 |
705 | throw:抛出异常,并退出当前方法或作用域
706 |
707 | 使用 finally 清理:
708 |
709 | 在 Java 中的 finally 的存在并不是为了释放内存资源,因为 Java 有垃圾回收机制,因此需要 Java 释放的资源主要是:已经打开的文件或者网络连接等。
710 |
711 | 在 try 中无论有没有捕获异常,finally 都会被执行。
712 |
713 | 参考自:[https://www.jianshu.com/p/ffca876ce719](https://www.jianshu.com/p/ffca876ce719)
714 |
715 |
716 |
717 | ### 6. 谈谈你对解析与分派的认识
718 |
719 | [深入理解Java虚拟机之:多态性实现机制----静态分派与动态分配](https://www.jianshu.com/p/1976b01c07d2)
720 |
721 |
722 |
723 | ### 7. 修改对象A的equals方法的签名,那么使用HashMap存放这个对象实例的时候,会调用哪个equals方法?
724 |
725 |
726 |
727 | ### 8. Java中实现多态的机制是什么?
728 |
729 |
730 |
731 | ### 9. 如何将一个Java对象序列化到文件里?
732 |
733 |
734 |
735 | ### 10. 说说你对Java反射的理解
736 |
737 | 反射:在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
738 |
739 | 关于反射的基本使用可看:[Java-反射的理解与使用-(原创)](https://www.jianshu.com/p/2302ad5dbfad)
740 |
741 | **反射是一种具有与类进行动态交互能力的一种机制**。
742 |
743 | **反射的作用:**
744 |
745 | 在 Java 和 Android 开发中,一般情况下下面几种场景会用到反射机制:
746 |
747 | * 需要访问隐藏属性或者调用方法改变程序原来的逻辑,这个在开发中是很常见的,由于一些原因,系统并没有开放一些接口出来,这个时候利用反射是一个有效的解决办法。
748 | * 自定义注解,注解就是在运行时利用反射机制来获取的。
749 | * 在开发中动态加载类,比如在 Android 中的动态加载解决65k问题等等,模块化和插件化都离不开反射。
750 |
751 | **反射的工作原理**
752 |
753 | 我们知道,每个 Java 文件最终都会被编译成一个 .class 文件,这些 Class 对象承载了这个类的所有信息,包括父类、接口、构造函数、方法、属性等等,这些class文件在程序运行时会被 ClassLoader 加载到虚拟机中。当一个类被加载以后,Java 虚拟机就会在内存中自动产生一个 Class 对象,而一般情况下用 new 来创建对象,实际上本质都是一样的,只是底层原理对我们开发者透明罢了。有了 class 对象的引用,就相当于有了 Method、Field、Constructor 的一切信息,在 Java 中,有了对象的引用就有了一切,剩下就靠自己发挥了。
754 |
755 | 参考自:[Java反射以及在Android中的特殊应用](https://www.jianshu.com/p/8f394d90a85c)
756 |
757 |
758 |
759 | ### 11. 说说你对Java注解的理解
760 |
761 | 注解(Annotion)是一个接口(前加 @),程序可以通过反射来获取指定程序元素的Annotion对象,然后通过Annotion对象来获取注解里面的元数据。
762 |
763 | 注解可以用于创建文档,跟踪代码中的依赖性,甚至执行基本编译时检查。从某些方面看,Annotion就像修饰符一样被使用,并应用于包、类型、构造方法、方法、成员变量、参数、本地变量的声明中。
764 |
765 | Annotation 的行为十分类似 public、final 这样的修饰符。每个 Annotation 具有一个名字和成员,每个 Annotation 的成员具有被称为 name=value 对的名字和值,name=value 装载了 Annotation 的信息,也就是说注解中可以不存在成员。
766 |
767 | 使用注解的基本规则:Annotation 不能影响程序代码的执行,无论增加、删除 Annotation,代码都始终如一的执行。
768 |
769 | 关于注解的基本使用可见:[https://www.jianshu.com/p/91393daaaf32](https://www.jianshu.com/p/91393daaaf32)
770 |
771 | 参考自:[https://www.jianshu.com/p/8da24b7cf443](https://www.jianshu.com/p/8da24b7cf443)
772 |
773 | 从JDK5开始,Java增加了Annotation,注解是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。通过使用Annotation,开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充的信息。
774 |
775 | Annotation提供了一种为程序元素(包、类、构造器、方法、成员变量、参数、局部变量)设置元数据的方法。Annotation不能运行,**它只有成员变量,没有方法**。Annotation跟public、final修饰符一样,都是程序元素的一部分,Annotation不能作为一个程序元素使用。
776 |
777 | #### 1. 定义Annotation
778 |
779 | 定义新的Annotation类型使用@interface关键字(在原有interface关键字前增加@符合),例如:
780 |
781 | ```java
782 | //定义注解
783 | public @interface Test{
784 | }
785 | //使用注解
786 | @Test
787 | public class MyClass{
788 | ....
789 | }
790 | ```
791 |
792 | **1.1 成员变量**
793 |
794 | Annotation只有成员变量,没有方法。Annotation的成员变量在定义中以“无形参的方法”形式声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。例如:
795 |
796 | ```java
797 | //定义注解
798 | public @interface MyTag{
799 | string name();
800 | int age();
801 | string id() default 0;
802 | }
803 | //使用注解
804 | public class Test{
805 | @MyTag(name="红薯",age=30)
806 | public void info(){
807 | ......
808 | }
809 | }
810 | ```
811 |
812 | 一旦在Annotation里定义了成员变量后,使用该Annotation时就应该为该Annotation的成员变量指定值。
813 |
814 | 也可以在定义Annotation的成员变量时,为其指定默认值,指定成员变量默认值使用default关键字。
815 |
816 | 根据Annotation是否包含了成员变量,可以把Annotation分为以下两类:
817 |
818 | * 标记Annotation
819 |
820 | 没有成员变量的Annotation被称为标记,这种Annotation仅用自身的存在与否来为我们提供信息,例如@override等
821 |
822 | * 元数据Annotation
823 |
824 | 包含成员变量的Annotation。因为它可以接收更多的元数据,因此被称为元数据Annotation。
825 |
826 | **1.2 元注解**
827 |
828 | 在定义Annotation时,也可以使用JDK提供的元注解来修饰Annotation定义。JDK提供了如下四个元注解(注解的注解,不是上诉的“元数据”)
829 |
830 | * @Retention
831 | * @Target
832 | * @Documented
833 | * @Inherited
834 |
835 | **1.2.1 @Retention**
836 |
837 | 用于指定Annotation可以保留多长时间。
838 |
839 | @Retention包含一个名为“value”的成员变量,该value成员变量是RetentionPolicy枚举类型。使用@Retention时,必须为其value指定值,value成员变量的值只能是如下三个:
840 |
841 | * RetentionPolicy.SOURCE
842 |
843 | Annotation只保留在源代码中,编译器编译时,直接丢弃这种Annotation
844 |
845 | * RetentionPolicy.CLASS
846 |
847 | 编译器会把Annotation记录在class文件中,当运行Java程序时,JVM中不再保留该Annotation
848 |
849 | * RetentionPolicy.RUNTIME
850 |
851 | 编译器把Annotation记录在class文件中,当运行Java程序时,JVM会保留该Annotation,程序可以通过反射获取该Annotation的信息。
852 |
853 | ```
854 | //name=value形式
855 | //@Retention(value=RetentionPolicy.RUNTIME)
856 |
857 | //直接指定
858 | @Retention(RetentionPolicy.RUNTIME)
859 | public @interface MyTag{
860 | String name() default "我兰";
861 | }
862 | ```
863 |
864 | **1.2.2 @Target**
865 |
866 | @Target指定Annotation用于修饰哪些程序元素。@Target也包含一个名为“value”的成员变量,该value成员变量类型为ElementType[],同样为枚举类型,值有以下几个:
867 |
868 | * ElementType.TYPE 能修饰类、接口或枚举类型
869 | * ElementType.FIELD 能修饰成员变量
870 | * ElementType.METHOD 能修饰方法
871 | * ElementType.PARAMETER 能修饰参数
872 | * ElementType.CONSTRUCTOR 能修饰构造器
873 | * ElementType.LOCAL_VARIABLE 能够修饰局部变量
874 | * ElementType.ANNOTATION_TYPE 能修饰注解
875 | * ElementType.PACKAGE 能修饰包
876 |
877 | ```java
878 | //单个ElementType
879 | @Target(ElementType.FIELD)
880 | public @interface AnnTest {
881 | String name() default "sunchp";
882 | }
883 | //多个ElementType
884 | @Target({ ElementType.FIELD, ElementType.METHOD })
885 | public @interface AnnTest {
886 | String name() default "sunchp";
887 | }
888 | ```
889 |
890 | **1.2.3 @Documented**
891 |
892 | 如果定义注解时,使用了@Documented修饰定义,则在用javadoc命令生成API文档后,所有使用该注解修饰的程序元素,将会包含该注解的说明。
893 |
894 | **1.2.4 @Inherited**
895 |
896 | 指定Annotation具有继承性。
897 |
898 |
899 |
900 | **1.3 基本Annotation**
901 |
902 | * @Override
903 |
904 | 限定重写父类方法。对于子类中被@Override修饰的方法,如果存在对应的被重写的父类方法,则正确;如果不存在,则报错。@Override只能作用于方法,不能作用于其他程序元素。
905 |
906 | * @Deprecated
907 |
908 | 用于表示某个程序元素(类、方法等等)已过时,如果使用被@Deprecated修饰的类或方法等,编译器会发出警号。
909 |
910 | * @SuppressWarning
911 |
912 | 抑制编译器警号。指示被@SuppressWarning修饰的程序元素(以及该程序元素中的所有子元素,例如类以及该类中的方法...)取消显示指定的编译器警告,例如,常见的@SuppressWarning (value="unchecked")
913 |
914 | * @SafeVarargs
915 |
916 | 告诉编译器忽略可变长度参数可能引起的类型转换问题,该注解修饰的方法必须为 static。
917 |
918 | #### 2. 提取Annotation信息(反射)
919 |
920 | 当开发者使用了Annotation修饰了类、方法、Field等成员之后,这些Annotation不会自己生效,必须由开发者提供相应的代码来提取并处理Annotation信息,这些处理和提取Annotation的代码统称为APT(Annotation Processing Tool)。
921 |
922 | JDK主要提供了两个类,来完成Annotation的提取:
923 |
924 | * java.lang.annotation.Annotation
925 |
926 | 这个接口是所有Annotation类型的父接口
927 |
928 | * java.lang.reflect.AnnotatedElement
929 |
930 | 该接口代表程序中被注解的程序元素
931 |
932 | **2.1 java.lang.annotation.Annotation**
933 |
934 | 该接口源码:
935 |
936 | ```java
937 | package java.lang.annotation;
938 |
939 | public interface Annotation {
940 |
941 | boolean equals(Object obj);
942 |
943 | int hashCode();
944 |
945 | String toString();
946 |
947 | Class extends Annotation> annotationType();
948 | }
949 | ```
950 |
951 | 其中主要方法是annotationType(),用于返回该注解的java.lang.Class
952 |
953 | **2.2 java.lang.reflect.AnnotatedElement**
954 |
955 | 接口源码:
956 |
957 | ```java
958 | package java.lang.reflect;
959 |
960 | import java.lang.annotation.Annotation;
961 |
962 | public interface AnnotatedElement {
963 |
964 | //判断该程序元素上是否存在指定类型的注解,如果存在返回true,否则false
965 | boolean isAnnotationPresent(Class extends Annotation> annotationClass);
966 |
967 | //返回该程序元素上存在的指定类型的注解,如果该类型的注解不存在,则返回null
968 | T getAnnotation(Class annotationClass);
969 |
970 | //返回该程序元素上存在的所有注解
971 | Annotation[] getAnnotations();
972 |
973 | Annotation[] getDeclaredAnnotations();
974 | }
975 | ```
976 |
977 | AnnotatedElement接口是所有程序元素(例如java.lang.Class、java.lang.reflect.Method、java.lang.reflect.Constructor等)的父接口。所以程序通过反射获取某个类的AnnotatedElement对象后,就可以调用该对象的isAnnotationPresent()、getAnnotation()等方法来访问注解信息。
978 |
979 | **为了获取注解信息,必须使用反射知识。**
980 |
981 | 注:如果想要在运行时获取注解信息,在定义注解的时候,该注解必须要使用@Retention(RetentionPolicy.RUNTIME)修饰。
982 |
983 | **2.3 示例**
984 |
985 | **2.3.1 标记Annotation**
986 |
987 | ```java
988 | //定义注解
989 | @Retention(RetentionPolicy.RUNTIME)
990 | @Target(ElementType.METHOD)
991 | public @interface MyTag {
992 |
993 | }
994 | //注解处理
995 | class ProcessTool {
996 | static void process(String clazz) {
997 | Class targetClass = null;
998 | try {
999 | targetClass = Class.forName(clazz);
1000 | } catch (ClassNotFoundException e) {
1001 | e.printStackTrace();
1002 | }
1003 | assert targetClass != null;
1004 | for (Method method : targetClass.getMethods()) {
1005 | if (method.isAnnotationPresent(MyTag.class)) {
1006 | System.out.println("被MyTag注解修饰的方法名:" + method.getName());
1007 | } else {
1008 | System.out.println("没有被MyTag注解修饰的方法名:" + method.getName());
1009 | }
1010 | }
1011 | }
1012 | }
1013 | //测试类
1014 | public class TagTest {
1015 |
1016 | @MyTag
1017 | public static void m1() {
1018 |
1019 | }
1020 |
1021 | public static void m2() {
1022 |
1023 | }
1024 |
1025 | public static void main(String[] args) {
1026 | ProcessTool.process("annotation.TagTest");
1027 | }
1028 | }
1029 | //输出
1030 | 没有被MyTag注解修饰的方法名:main
1031 | 被MyTag注解修饰的方法名:m1
1032 | 没有被MyTag注解修饰的方法名:m2
1033 | 没有被MyTag注解修饰的方法名:wait
1034 | 没有被MyTag注解修饰的方法名:wait
1035 | 没有被MyTag注解修饰的方法名:wait
1036 | 没有被MyTag注解修饰的方法名:equals
1037 | 没有被MyTag注解修饰的方法名:toString
1038 | 没有被MyTag注解修饰的方法名:hashCode
1039 | 没有被MyTag注解修饰的方法名:getClass
1040 | 没有被MyTag注解修饰的方法名:notify
1041 | 没有被MyTag注解修饰的方法名:notifyAll
1042 | ```
1043 |
1044 | **2.3.2 元数据Annotation**
1045 |
1046 | ```java
1047 | //定义注解
1048 | @Retention(RetentionPolicy.RUNTIME)
1049 | @Target(ElementType.METHOD)
1050 | public @interface MyTag {
1051 | String name() default "Omooo";
1052 |
1053 | int age() default 21;
1054 | }
1055 | //注解处理
1056 | class ProcessTool {
1057 | static void process(String clazz) {
1058 | Class targetClass = null;
1059 | try {
1060 | targetClass = Class.forName(clazz);
1061 | } catch (ClassNotFoundException e) {
1062 | e.printStackTrace();
1063 | }
1064 | assert targetClass != null;
1065 | for (Method method : targetClass.getMethods()) {
1066 | if (method.isAnnotationPresent(MyTag.class)) {
1067 | MyTag tag = method.getAnnotation(MyTag.class);
1068 | System.out.println("方法:" + method.getName() + " 的注解内容为:" + tag.name() + " " + tag.age());
1069 | }
1070 | }
1071 | }
1072 | }
1073 | //测试类
1074 | public class TagTest {
1075 |
1076 | @MyTag
1077 | public static void m1() {
1078 |
1079 | }
1080 |
1081 | @MyTag(name = "当当猫", age = 20)
1082 | public static void m2() {
1083 |
1084 | }
1085 |
1086 | public static void main(String[] args) {
1087 | ProcessTool.process("annotation.TagTest");
1088 | }
1089 | }
1090 | //输出
1091 | 方法:m1 的注解内容为:Omooo 21
1092 | 方法:m2 的注解内容为:当当猫 20
1093 | ```
1094 |
1095 | #### 3. 注解本质
1096 |
1097 | * 注解实质上会被编译器编译为接口,并且继承java.lang.annotation.Annotation接口
1098 | * 注解的成员变量会被编译器编译成同名的抽象方法
1099 | * 根据Java的class文件规范,class文件中会在程序元素的属性位置记录注解信息。
1100 |
1101 | #### 4. 注解的意义
1102 |
1103 | 1. 为编译器提供辅助信息
1104 |
1105 | Annotation可以为编译器提供额外的信息,以便于检测错误,抑制警告等。
1106 |
1107 | 2. 编译源代码时进行额外操作
1108 |
1109 | 软件工具可以通过处理Annotation信息来生成源代码,xml文件等等。
1110 |
1111 | 3. 运行时处理
1112 |
1113 | 有一些Annotation甚至可以在程序运行时被检测、使用。
1114 |
1115 |
1116 | 总之,注解是一种元数据,起到了“描述、配置”的作用。
1117 |
1118 |
1119 | 引自:[http://www.open-open.com/lib/view/open1423558996951.html](http://www.open-open.com/lib/view/open1423558996951.html)
1120 |
1121 | ### 12. 说说你对依赖注入的理解
1122 |
1123 | 依赖注入:可以通过这个服务来安全的注入组件到应用程序中,在应用程序部署的时候还可以选择从特定的接口属性进行注入。
1124 |
1125 | 参考自:
1126 |
1127 | [用Dagger2在Android中实现依赖注入](https://www.jianshu.com/p/f713dd40e037)
1128 |
1129 | [反射、注解与依赖注入总结](https://www.jianshu.com/p/24820bf3df5c)
1130 |
1131 |
1132 |
1133 | ### 13. 说一下泛型原理,并举例说明
1134 |
1135 |
1136 |
1137 | ### 14. Java中String的了解
1138 |
1139 |
1140 |
1141 | ### 15. String为什么要设计成不可变的?
1142 |
1143 | String 是 Java 中一个不可变类,所以他一旦被实例化就无法被修改。为什么要把String设计成不可变的呢?那就从内存、同步和数据结构层面上谈起。
1144 |
1145 | **字符串池**
1146 |
1147 | 字符串池是方法区中的一部分特殊存储。当一个字符串被创建的时候,首先会去这个字符串池中查找。如果找到,直接返回对该字符串的引用。
1148 |
1149 | 下面的代码只会在堆中创建一个字符串:
1150 |
1151 | ```java
1152 | String string1 = "abcd";
1153 | String string2 = "abcd";
1154 | ```
1155 |
1156 | 
1157 |
1158 | **如果字符串可变的话,当两个引用指向同一个字符串时,对其中一个做修改就会影响另外一个。**
1159 |
1160 | ```java
1161 | String str= "123";
1162 | str = "456";
1163 | ```
1164 |
1165 | 执行过程如下:
1166 |
1167 | 
1168 |
1169 | 执行第一行代码时,在堆上新建一个对象实例 123,str 是一个指向该实例的引用,**引用包含的仅仅只是实例在堆上的内存地址而已**。执行第二行代码时,仅仅只是改变了 str 这个引用的地址,指向了另外一个实例 456。**给 String 赋值仅仅只是改变了它的引用而已,并不会真正的去改变它本来内存地址上的值**。这样的好处也是显而易见的,最简单的当存在多个 String 的引用指向同一个内存地址时,改变其中一个引用的值并不会其他引用的值造成影响。
1170 |
1171 | 那如果我们这样写呢:
1172 |
1173 | ```java
1174 | String str1 = "123";
1175 | String str2 = new String("123");
1176 | System.out.println(str1 == str2);
1177 | ```
1178 |
1179 | 结果是 false。JVM为了字符串的复用,减少字符串对象的重复创建,特别维护了一个字符串常量池。第一种字面量形式的写法,会直接在字符串常量池中查找是否存在值 123,若存在直接返回这个值的引用,若不存在则创建一个值123的String对象并存入字符串常量池中。而使用new关键字,则会直接才堆上生成一个新的String对象,并不会理会常量池中是否有这个值。所以本质上 str1 和 str2 指向的内存地址是不一样的。
1180 |
1181 | 那么,使用 new 关键字生成的 String 对象可以进入字符串常量池吗?答案是肯定的,String 类提供了一个 native 方法 intern() 用来将这个对象加入字符串常量池:
1182 |
1183 | ```java
1184 | String str1 = "123";
1185 | String str2 = new String("123");
1186 | str2=str2.intern();
1187 | System.out.println(str1 == str2);
1188 | ```
1189 |
1190 | 结果为 true。str2 调用 intern() 方法后,首先会在字符串常量池中寻找是否存在值为 123 的对象,若存在直接返回该对象的引用,若不存在,加入 str2 并返回。上诉代码中,常量池中已经存在了值为 123 的 str1 对象,则直接返回 str1 的引用地址,使得 str1 和 str2 指向同一个内存地址。
1191 |
1192 | **缓存 Hashcode**
1193 |
1194 | Java 中经常会用到字符串的哈希码。例如,在HashMap中,字符串的不可变能保证其Hashcode永远保持一致,避免了一些不必要的麻烦。这也就意味着每次使用一个字符串的hashcode的时候不用重新计算一次,更加高效。
1195 |
1196 | **安全性**
1197 |
1198 | String被广泛的使用在其他类中充当参数,如果字符串可变,那么类似操作就会可能导致安全问题。可变的字符串也可能导致反射的安全问题,因为它的参数也是字符串。
1199 |
1200 | **不可变对象天生就是线程安全的**
1201 |
1202 | 因为不可变对象不能被改变,所以他们可以自由的在多个线程之间共享,不需要做任何同步处理。
1203 |
1204 | 总之,String被设计成不可变的主要目的是为了安全和高效。当然,缺点就是要创建多余的对象而并非改变其值。
1205 |
1206 | 参考自:
1207 |
1208 | [https://www.jianshu.com/p/b1d62928552d](https://www.jianshu.com/p/b1d62928552d)
1209 |
1210 | [http://www.hollischuang.com/archives/1246](http://www.hollischuang.com/archives/1246)
1211 |
1212 |
1213 |
1214 | ### 16. Object类的equal和hashCode方法重写,为什么?
1215 |
1216 |
1217 |
1218 | ### 17. 为什么Java里的匿名内部类只能访问final修饰的外部变量?
1219 |
1220 | 因为匿名内部类最终会被编译成一个单独的类,而被该类使用的变量会以构造函数参数的形式传递给该类。如果变量不定义为final的,参数在匿名内部类中可以被修改,进而造成和外部的变量不一致的问题,为了避免这种不一致的情况,规定匿名内部类只能访问final修饰的外部变量。
1221 |
1222 |
1223 |
1224 | ### 18. Synchronized
1225 |
1226 | synchronized,是Java中用于解决并发情况下数据同步访问的一个很重要的关键字。当我们想要保证一个共享资源在同一个时间只会被一个线程访问到时,我们可以在代码中使用synchronized关键字对类或者对象加锁。
1227 |
1228 | 在Java中,synchronized有两种使用方式,同步方法和同步代码块。
1229 |
1230 | 对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步。对于同步代码块,JVM采用monitorenter、monitorexit 两个指令来实现同步。
1231 |
1232 | **同步方法**
1233 | 方法级的同步是隐式的,同步方法的常量池中会有一个ACC_SYNCHRNZED 标志,当某个线程要访问某个方法的时候,会检查是否有 ACC_SYNCHORIZED ,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后在释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果方法在执行过程中发生了异常,并且方法内部并没有处理该异常,那么异常被抛到方法外面之前监视器锁会被自动释放。
1234 |
1235 | **同步代码块**
1236 | 同步代码块使用monitorenter和monitorexit两个指令实现。可以把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。每个对象维护着一个记录被锁次数的计数器,未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为1,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器在自减。当计数器为0的时候,锁将被释放,其他线程便可以获得锁。
1237 |
1238 | **synchronized与原子性**
1239 | 原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。
1240 |
1241 | 线程是CPU调度的基本单位,CPU有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。
1242 |
1243 | 在Java中,为了保证原子性,提供了两个高级的字节码指令 monitorenter 和 monitorexit 。前面介绍过,这两个字节码指令,在Java中对应的关键字就是 synchronized。
1244 |
1245 | 通过 monitorenter 和 monitorexit 指令,可以保证被 synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在Java中可以使用 synchronized 来保证方法和代码块内的操作是原子性的。
1246 |
1247 | 线程一在执行monitorenter指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程一主动解锁。即使在执行过程中,由于某种原因,比如CPU时间片用完,线程一放弃了CPU,但是,他并没有进行解锁,而由于 synchorized 的锁是可以重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码,直到所有代码执行完,这就保证了原子性。
1248 |
1249 | **synchroized与可见性**
1250 | 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
1251 |
1252 | Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之后也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主内存之间进行数据同步进行。所以,就可能出现线程一修改了某个变量的值,但线程二不可见的情况。
1253 |
1254 | 前面我们介绍过,被 synchronized 修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。而为了保证可见性,有一条规则是专业的:对一个变量解锁之前,必须先把变量同步到主内存中。这样解锁后,后续线程就可以访问到被修改后的值。
1255 |
1256 | 所以,被 synchronized 关键字锁住的对象,其值是具有可见性的。
1257 |
1258 | **synchronized与有序性**
1259 | 有序性即程序执行的顺序按照代码的先后顺序执行。
1260 |
1261 | 除了引入了时间片以外,由于处理器优化和指令重排,CPU还可能对输入代码进行乱序执行,这就可能存在有序性问题。
1262 |
1263 | 这里需要注意的是,synchronized 是无法禁止指令重排和处理器优化的,也就是说,synchronized 无法避免上述提到的问题。那么为什么还说synchronized 也提供了有序性保证呢?
1264 |
1265 | 这就要把有序性的概念扩展一下了,Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有操作都是天然有序的,如果在一个线程中观察另外一个线程,所有的操作都是无序的。
1266 |
1267 | 以上这句话也是《深入理解Java虚拟机》中的原句,但是怎么理解呢?这其实和 as-if-serial语义有关。
1268 |
1269 | as-if-serial 语义的意思是指:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变,编译器和处理器无论如何优化,都必须遵守 as-if-serial语义。
1270 |
1271 | 简单来说,as-if-serial语义保证了单线程中,指令重排是有一定限制的,而只要编译器和处理器都遵守这个语义,那么就可以认为单线程程序是按照顺序执行的,当然,实际上还是有重排,只不过我们无需关心这种重排的干扰。
1272 |
1273 | 所以说,由于synchronized修饰的代码,同一时间只能被同一个线程访问,那么也就是单线程执行,所以可以保证其有序性。
1274 |
1275 | **synchronized与锁优化**
1276 |
1277 | 无论是ACC_SYNCHORIZED还是monitorenter、monitorexit都是基于Monitor实现的,在Java虚拟机(HotSpot)中,Monitor是基于C++实现的,由ObjectMonitor实现。
1278 |
1279 | ObjectMonitor类中提供了几个方法,如 enter、exit、wait、notify、notifyAll 等。sychronized 加锁的原理,会调用 objectMonitor的enter方法,解锁的时候会调用 exit 方法。事实上,只有在JDK1.6之前,synchronized的实现才会直接调用ObjectMonitor的enter和exit,这种锁被称之为重量级锁。为什么说这种方式操作锁很重呢?
1280 |
1281 | Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就是要从用户态转换为核心态,因此状态转换需要花费很多的处理器时间,对于代码简单的同步块,状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说synchroized是java语言中一个重量级的操纵。
1282 |
1283 | 所以,在JDK1.6中出现对锁进行了很多的优化,进而出现了轻量级锁、偏向锁、锁消除,适应性自旋锁等等,这些操作都是为了在线程之间更高效的共享数据,解决竞争问题。
1284 |
1285 | ### 19. volatile
1286 |
1287 | **volatile用法**
1288 | volatile通常被比喻成轻量级的synchronized,也是Java并发编程中比较重要的一个关键字,和synchronized不同,volatile是一个变量修饰符,只能用来修饰变量,无法修饰方法以及代码块。
1289 |
1290 | volatile的用法比较简单,只需要在声明一个可能被多线程同时访问的变量时,使用volatile修饰就可以了。
1291 |
1292 | **volatile原理**
1293 | 为了提高处理器的执行速度,在处理器和内存之间增加了多级缓存来提升,但是由于引入了多级缓存,就存在缓存数据不一致的问题。
1294 |
1295 | 但是,对于volatile变量,当对volatile变量进行写操作的时候,JVM会向处理器发送一条Lock前缀的指令,将这个缓存中的变量回写到系统主存中。
1296 |
1297 | 但是就算回写内存,如果其他处理器缓存的值还是旧的,在执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议。
1298 |
1299 | **缓存一致性协议:**
1300 |
1301 | 每个处理器通过嗅探在总线上传播的数据来检测自己缓存的信息是不是过期了,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置为无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
1302 |
1303 | 所以,如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷新入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中,这就保证了一个volatile修饰的变量在多个缓存中是可见的。
1304 |
1305 | **volatile与可见性**
1306 | 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程也能够立即看到修改的值。
1307 |
1308 | 前面在关于volatile原理的时候讲过,Java中的volatile关键字提供了一个功能,那就是被修饰的变量在被修改后可以立即同步到主存中,被其修饰的变量在每次使用之前都是从主内存中刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。
1309 |
1310 | **volatile与有序性**
1311 | volatile禁止指令重排优化,这就保证了代码的程序会严格按照代码的先后顺序执行,这就保证了有序性。
1312 |
1313 | **volatile与原子性**
1314 | 在上面介绍synchronized的时候,提到过,为了保证原子性,需要通过字节码指令monitorenter和monitorexit,但是volatile和这两个指令没有任何关系。
1315 |
1316 | **所以,volatile是不能保证原子性的。**
1317 |
1318 | 在以下两个场景中可以使用volatile替代synchronized:
1319 |
1320 | 1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程会修改变量的值
1321 | 2. 变量不需要与其他状态变量共同参与不变约束
1322 |
1323 | 除了以上场景,都需要使用其他方式来保证原子性,如synchronized或者concurrent包。
1324 |
1325 | synchronized可以保证原子性、有序性和可见性,而volatile只能保证有序性和可见性。
1326 |
1327 |
1328 |
1329 | ### 20. 类加载流程双亲委托机制
1330 |
1331 | **类加载流程**
1332 |
1333 | 1. 装载
1334 | 2. 链接
1335 | 1. 验证
1336 | 2. 准备
1337 | 3. 解析
1338 | 3. 初始化
1339 |
1340 | **双亲委托机制**
1341 |
1342 | 某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才会自己去加载。
1343 |
1344 | 使用双亲委托模型的好处在于Java类随着它的类加载器一起具备类一种带有优先级的层次关系。例如类java.lang.Object,它存在rt.jar中,无论哪一个类加载器要加载这个类,最终都会委托处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各类加载器环境中都是同一个类。相反,如果没有双亲委派模型,而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那么系统中将会出现多个不同的Object类,程序将变得混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。
1345 |
1346 | 1. 当前ClassLoader首先从自己已经加载的类中查询是否此类已经记载,如果已经加载则可以直接返回已经加载的类。
1347 |
1348 | 每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。
1349 |
1350 | 2. 当前ClassLoader的缓存中没有找到被加载的类的时候,会委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到BootStrap ClassLoader。
1351 |
1352 | 3. 当所有的父类加载器都没有加载的时候,再有当前的类加载器加载,并将其放入它自己的缓存中,以便下次加载请求时直接返回。
1353 |
1354 | 为什么需要这样的委托机制呢?理解这个问题,我们要引入另外一个关于ClassLoader的概念“命名空间”,它是指要确定某一个类,需要类的全限定名以及加载此类的ClassLoader来共同确定。也就是说,即使两个类的全限定名相同,但是因为不同的ClassLoader加载了此类,那么在JVM中它是不同的类。明白了命名空间以后,我们再来看看委托模型。采用了委托模型以后加大了不同的ClassLoader的交互能力,比如上面说的,我们JDK本身提供的类库,比如HashMap、LinkedList等等,这些类由bootstrap类加载器加载以后,无论你程序中有多少个类加载器,那么这些类其实都是可以共享的,这样就避免了不同的类加载器加载了同样名字的不同类以后造成混乱。
1355 |
1356 | ## Java 部分(三)数据结构
1357 |
1358 | ### 1. 常用数据结构简介
1359 |
1360 |
1361 |
1362 | ### 2. 并发集合了解哪些?
1363 |
1364 |
1365 |
1366 | ### 3. HashMap
1367 |
1368 | **简介**
1369 |
1370 | HashMap 是一散列表,它存储的内容是键值对的映射。它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序确是不确定的。
1371 |
1372 | HashMap最多只允许一条记录的键为null,允许多条记录的值为null。
1373 |
1374 | HashMap使用hash算法进行数据的存储和查询,内部使用一个Entry表示键值对key-value。用Entry的数组保存所有键值对,Entry通过链表的方式链接后续的节点(1.8后会根据链表长度决定是否转换成红黑树),Entry通过计算key的hash值来决定映射到具体的哪个数组(也叫Bucket)中。
1375 |
1376 | HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致,如果需要满足线程安全,可以用Collections的synchronizedMap方法使得HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
1377 |
1378 | **存储结构**
1379 | HashMap是数组+链表+红黑树实现的。
1380 |
1381 | ```java
1382 | public class HashMap extends AbstractMap implements Map, Cloneable, Serializable {
1383 | private static final long serialVersionUID = 362498820763181265L;
1384 | /**
1385 | * HashMap 的默认初始容量为 16,必须为 2 的 n 次方 (一定是合数)
1386 | */
1387 | static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
1388 |
1389 | /**
1390 | * HashMap 的最大容量为 2 的 30 次幂
1391 | */
1392 | static final int MAXIMUM_CAPACITY = 1 << 30;
1393 |
1394 | /**
1395 | * HashMap 的默认负载因子
1396 | */
1397 | static final float DEFAULT_LOAD_FACTOR = 0.75f;
1398 |
1399 | /**
1400 | * 链表转成红黑树的阈值。即在哈希表扩容时,当链表的长度(桶中元素个数)超过这个值的时候,进行链表到红黑树的转变
1401 | */
1402 | static final int TREEIFY_THRESHOLD = 8;
1403 |
1404 | /**
1405 | * 红黑树转为链表的阈值。即在哈希表扩容时,如果发现链表长度(桶中元素个数)小于 6,则会由红黑树重新退化为链表
1406 | */
1407 | static final int UNTREEIFY_THRESHOLD = 6;
1408 |
1409 | /**
1410 | * HashMap 的最小树形化容量。这个值的意义是:位桶(bin)处的数据要采用红黑树结构进行存储时,整个Table的最小容量(存储方式由链表转成红黑树的容量的最小阈值)
1411 | * 当哈希表中的容量大于这个值时,表中的桶才能进行树形化,否则桶内元素太多时会扩容,而不是树形化
1412 | * 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
1413 | */
1414 | static final int MIN_TREEIFY_CAPACITY = 64;
1415 |
1416 | /**
1417 | * Node 是 HashMap 的一个内部类,实现了 Map.Entry 接口,本质是就是一个映射 (键值对)
1418 | * Basic hash bin node, used for most entries.
1419 | */
1420 | static class Node implements Map.Entry {
1421 | final int hash; // 用来定位数组索引位置
1422 | final K key;
1423 | V value;
1424 | Node next; // 链表的下一个node
1425 |
1426 | Node(int hash, K key, V value, Node next) { ... }
1427 |
1428 | public final K getKey() { ... }
1429 | public final V getValue() { ... }
1430 | public final String toString() { ... }
1431 | public final int hashCode() { ... }
1432 | public final V setValue(V newValue) { ... }
1433 | public final boolean equals(Object o) { ... }
1434 | }
1435 |
1436 | /**
1437 | * 哈希桶数组,分配的时候,table的长度总是2的幂
1438 | */
1439 | transient Node[] table;
1440 |
1441 | /**
1442 | * Holds cached entrySet(). Note that AbstractMap fields are used
1443 | * for keySet() and values().
1444 | */
1445 | transient Set> entrySet;
1446 |
1447 | /**
1448 | * HashMap 中实际存储的 key-value 键值对数量
1449 | */
1450 | transient int size;
1451 |
1452 | /**
1453 | * 用来记录 HashMap 内部结构发生变化的次数,主要用于迭代的快速失败机制
1454 | */
1455 | transient int modCount;
1456 |
1457 | /**
1458 | * HashMap 的门限阀值/扩容阈值,所能容纳的 key-value 键值对极限,当size>=threshold时,就会扩容
1459 | * 计算方法:容量capacity * 负载因子load factor
1460 | */
1461 | int threshold;
1462 |
1463 | /**
1464 | * HashMap 的负载因子
1465 | */
1466 | final float loadFactor;
1467 | }
1468 |
1469 | 作者:野狗子嗷嗷嗷
1470 | 链接:https://www.jianshu.com/p/b40fd341711e
1471 | 來源:简书
1472 | 简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
1473 | ```
1474 |
1475 | Node[] table的初始化长度是16,负载因子默认是0.75,threshold是HashMap所能容纳的最大键值对。threshold=length*loadFactor,也就是说当HashMap存储的元素数量大于threshold时,HashMap就会进行扩容的操作。
1476 |
1477 | size这个字段其实很好理解,就是HashMap中实际存在的键值对数量。而modCount字段主要用来记录HashMap内部结构发生变化的次数,主要用于迭代的快速失败。
1478 |
1479 | 在HashMap中,哈希桶数组table的长度length大小必须为2的n次方,这是一种非常规的设计,常规的设计是把桶的大小设计为素数,相对来说,素数导致冲突的概率要小。HashTable初始化桶大小为11,这就是桶大小设计为素数的应用(HashTable扩容后不能保证还是素数)。HashMap采用这种非常规的设计,主要是为了在取模和扩容时做优化,同时为了减少冲突,HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。
1480 |
1481 | **功能实现**
1482 |
1483 | 解决Hash冲突的hash()方法:
1484 |
1485 | HashMap的hash计算时先计算hashCode(),然后进行二次hash。
1486 |
1487 | ```java
1488 | // 计算二次Hash
1489 | int hash = hash(key.hashCode());
1490 |
1491 | // 通过Hash找数组索引
1492 | int i = hash & (tab.length-1);
1493 |
1494 | static final int hash(Object key) {
1495 | int h;
1496 | return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
1497 | }
1498 | ```
1499 |
1500 | 它总是通过h&(table.length-1)来得到该对象的保存位置,而HashMap底层数组的长度总是2的n次方,这样保证了计算得到的索引值总是位于table数组的索引之内。
1501 |
1502 | **put方法**
1503 |
1504 | 1. 对key的hashCode做hash,然后计算index
1505 | 2. 如果没碰撞,就直接放到bucket里,如果碰撞了,以链表的形式存在buckets后
1506 | 3. 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD=8),就把链表转换为红黑树
1507 | 4. 如果节点已经存在,就替换,保证key的唯一性
1508 | 5. 如果bucket满了(超过loadFactor*currentCapacity),就要resize
1509 |
1510 | 具体步骤:
1511 |
1512 | 1. 如果table没有使用过的情况,(tab=table)==null||(n=length)==0,则以默认大小进行一次resize
1513 | 2. 计算key的hash值,然后获取底层table数组的第(n-1)&hash的位置的数组索引tab[i]处的数据,即hash对n取模的位置,依赖的是n为2的次方的这一条件
1514 | 3. 先检查该bucket第一个元素是否是和插入的key相等(如果是同一个对象则肯定equals)
1515 | 4. 如果不相等并且是TreeNode的情况,调用TreeNode的put方法
1516 | 5. 否则循环遍历链表,如果找到相等的key跳出循环否则达到最后一个节点时将新的节点添加到链表最后,当前面找到了相同的key的情况下替换这个节点的value为新的value
1517 | 6. 如果新增了key-value对,则增加size并且判断是否超过了threshold,如果超过了则需要进行resize扩容
1518 |
1519 | ```java
1520 | public V put(K key, V value) {
1521 | // 对key的hashCode()做hash
1522 | return putVal(hash(key), key, value, false, true);
1523 | }
1524 |
1525 | /**
1526 | * Implements Map.put and related methods
1527 | *
1528 | * @param hash hash for key
1529 | * @param key the key
1530 | * @param value the value to put
1531 | * @param onlyIfAbsent if true, don't change existing value
1532 | * @param evict if false, the table is in creation mode.
1533 | * @return previous value, or null if none
1534 | */
1535 | final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
1536 | boolean evict) {
1537 | Node[] tab; Node p; int n, i;
1538 | // table为空或者length=0时,以默认大小扩容,n为table的长度
1539 | if ((tab = table) == null || (n = tab.length) == 0)
1540 | n = (tab = resize()).length;
1541 | // 计算index,并对null做处理,table[i]==null
1542 | if ((p = tab[i = (n - 1) & hash]) == null)
1543 | // (n-1)&hash 与Java7中indexFor方法的实现相同,若i位置上的值为空,则新建一个Node,table[i]指向该Node。
1544 | // 直接插入
1545 | tab[i] = newNode(hash, key, value, null);
1546 | else {
1547 | // 若i位置上的值不为空,判断当前位置上的Node p 是否与要插入的key的hash和key相同
1548 | Node e; K k;
1549 | // 若节点key存在,直接覆盖value
1550 | if (p.hash == hash &&
1551 | ((k = p.key) == key || (key != null && key.equals(k))))
1552 | e = p;
1553 | // 判断table[i]该链是否是红黑树,如果是红黑树,则直接在树中插入键值对
1554 | else if (p instanceof TreeNode)
1555 | // 不同,且当前位置上的的node p已经是TreeNode的实例,则再该树上插入新的node
1556 | e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
1557 | // table[i]该链是普通链表,进行链表的插入操作
1558 | else {
1559 | // 在i位置上的链表中找到p.next为null的位置,binCount计算出当前链表的长度,如果继续将冲突的节点插入到该链表中,会使链表的长度大于tree化的阈值,则将链表转换成tree。
1560 | for (int binCount = 0; ; ++binCount) {
1561 | // 如果遍历到了最后一个节点,说明没有匹配的key,则创建一个新的节点并添加到最后
1562 | if ((e = p.next) == null) {
1563 | p.next = newNode(hash, key, value, null);
1564 | // 链表长度大于8转换为红黑树进行处理
1565 | if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
1566 | treeifyBin(tab, hash);
1567 | break;
1568 | }
1569 | // 遍历过程中若发现 key 已经存在直接覆盖 value 并跳出循环即可
1570 | if (e.hash == hash &&
1571 | ((k = e.key) == key || (key != null && key.equals(k))))
1572 | break;
1573 | p = e;
1574 | }
1575 | }
1576 | // 已经存在该key的情况时,将对应的节点的value设置为新的value
1577 | if (e != null) { // existing mapping for key
1578 | V oldValue = e.value;
1579 | if (!onlyIfAbsent || oldValue == null)
1580 | e.value = value;
1581 | afterNodeAccess(e);
1582 | return oldValue;
1583 | }
1584 | }
1585 | ++modCount;
1586 | // 插入成功后,判断实际存在的键值对数量 size 是否超多了最大容量 threshold,如果超过,进行扩容
1587 | if (++size > threshold)
1588 | resize();
1589 | afterNodeInsertion(evict);
1590 | return null;
1591 | }
1592 |
1593 | ```
1594 |
1595 | **get方法**
1596 | get(key)方法时获取key的hash值,计算hash&(n-1)得到在链表数组中的位置first=tab[hash&(n-1)],先判断first的key是否是参数key相等,不等就遍历后面的链表找到相同的key值返回对应的value值即可。
1597 |
1598 | ```java
1599 | public V get(Object key) {
1600 | Node e;
1601 | return (e = getNode(hash(key), key)) == null ? null : e.value;
1602 | }
1603 |
1604 | // 根据哈希表元素个数与哈希值求模(使用的公式是 (n - 1) &hash)得到 key 所在的桶的头结点,如果头节点恰好是红黑树节点,就调用红黑树节点的 getTreeNode() 方法,否则就遍历链表节点
1605 | final Node getNode(int hash, Object key) {
1606 | Node[] tab; Node first, e; int n; K k;
1607 | if ((tab = table) != null && (n = tab.length) > 0 &&
1608 | (first = tab[(n - 1) & hash]) != null) {
1609 | if (first.hash == hash && // always check first node
1610 | ((k = first.key) == key || (key != null && key.equals(k))))
1611 | return first;
1612 | if ((e = first.next) != null) {
1613 | if (first instanceof TreeNode)
1614 | return ((TreeNode)first).getTreeNode(hash, key);
1615 | do {
1616 | if (e.hash == hash &&
1617 | ((k = e.key) == key || (key != null && key.equals(k))))
1618 | return e;
1619 | } while ((e = e.next) != null);
1620 | }
1621 | }
1622 | return null;
1623 | }
1624 |
1625 | ```
1626 |
1627 | **resize方法**
1628 | 扩容就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组。
1629 |
1630 | 具体步骤:
1631 |
1632 | 1. 首先计算resize()后的新的capacity和threshold,如果原有的capacity大于零,则将capacity增加一倍,否则设置成默认的capacity
1633 | 2. 创建新的数组,大小是新的capacity
1634 | 3. 将旧的数组元素放置到新数组中
1635 |
1636 | ```java
1637 | final Node[] resize() {
1638 | // 将字段引用copy到局部变量表,这样在之后的使用时可以减少getField指令的调用
1639 | Node[] oldTab = table;
1640 | // oldCap为原数组的大小或当空时为0
1641 | int oldCap = (oldTab == null) ? 0 : oldTab.length;
1642 | int oldThr = threshold;
1643 | int newCap, newThr = 0;
1644 | if (oldCap > 0) {
1645 | if (oldCap >= MAXIMUM_CAPACITY) {
1646 | // 如果超过最大容量1>>30,无法再扩充table,只能改变阈值
1647 | threshold = Integer.MAX_VALUE;
1648 | return oldTab;
1649 | }
1650 | // 新的数组的大小是旧数组的两倍
1651 | else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
1652 | oldCap >= DEFAULT_INITIAL_CAPACITY)
1653 | // 当旧的的数组大小大于等于默认大小时,threshold也扩大一倍
1654 | newThr = oldThr << 1;
1655 | }
1656 | else if (oldThr > 0) // initial capacity was placed in threshold
1657 | newCap = oldThr;
1658 | else { // zero initial threshold signifies using defaults
1659 | // 初始化操作
1660 | newCap = DEFAULT_INITIAL_CAPACITY;
1661 | newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
1662 | }
1663 | if (newThr == 0) {
1664 | float ft = (float)newCap * loadFactor;
1665 | newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
1666 | (int)ft : Integer.MAX_VALUE);
1667 | }
1668 | threshold = newThr;
1669 | @SuppressWarnings({"rawtypes","unchecked"})
1670 | // 创建容量为newCap的newTab,并将oldTab中的Node迁移过来,这里需要考虑链表和tree两种情况。
1671 | Node[] newTab = (Node[])new Node[newCap];
1672 | table = newTab;
1673 | // 将原数组中的数组复制到新数组中
1674 | if (oldTab != null) {
1675 | for (int j = 0; j < oldCap; ++j) {
1676 | Node e;
1677 | if ((e = oldTab[j]) != null) {
1678 | oldTab[j] = null;
1679 | if (e.next == null)
1680 | // 如果e是该bucket唯一的一个元素,则直接赋值到新数组中
1681 | newTab[e.hash & (newCap - 1)] = e;
1682 | else if (e instanceof TreeNode)
1683 | // split方法会将树分割为lower 和upper tree两个树,如果子树的节点数小于了UNTREEIFY_THRESHOLD阈值,则将树untreeify,将节点都存放在newTab中。
1684 | // TreeNode的情况则使用TreeNode中的split方法将这个树分成两个小树
1685 | ((TreeNode)e).split(this, newTab, j, oldCap);
1686 | else { // preserve order 保持顺序
1687 | // 否则则创建两个链表用来存放要放的数据,hash值&oldCap为0的(即oldCap的1的位置的和hash值的同样的位置都是1,同样是基于capacity是2的次方这一前提)为low链表,反之为high链表, 通过这种方式将旧的数据分到两个链表中再放到各自对应余数的位置
1688 | Node loHead = null, loTail = null;
1689 | Node hiHead = null, hiTail = null;
1690 | Node next;
1691 | do {
1692 | next = e.next;
1693 | // 按照e.hash值区分放在loTail后还是hiTail后
1694 | if ((e.hash & oldCap) == 0) {
1695 | // 运算结果为0的元素,用lo记录并连接成新的链表
1696 | if (loTail == null)
1697 | loHead = e;
1698 | else
1699 | loTail.next = e;
1700 | loTail = e;
1701 | }
1702 | else {
1703 | // 运算结果不为0的数据,用li记录
1704 | if (hiTail == null)
1705 | hiHead = e;
1706 | else
1707 | hiTail.next = e;
1708 | hiTail = e;
1709 | }
1710 | } while ((e = next) != null);
1711 | // 处理完之后放到新数组中
1712 | if (loTail != null) {
1713 | loTail.next = null;
1714 | // lo仍然放在“原处”,这个“原处”是根据新的hash值算出来的
1715 | newTab[j] = loHead;
1716 | }
1717 | if (hiTail != null) {
1718 | hiTail.next = null;
1719 | // li放在j+oldCap位置
1720 | newTab[j + oldCap] = hiHead;
1721 | }
1722 | }
1723 | }
1724 | }
1725 | }
1726 | return newTab;
1727 | }
1728 |
1729 | ```
1730 |
1731 | **size方法**
1732 | HashMap的大小很简单,不是实时计算的,而是每次新增加Entry的时候,size就递增。删除的时候就递减,空间换时间的做法,因为它不是线程安全的,完全可以这么做,效率高。
1733 |
1734 | **面试问题**
1735 |
1736 | **1. 构造函数中initialCapacity与loadFactor两个参数**
1737 | HashMap(int initialCapacity,float loadFactor):构造一个指定容量和负载因子的空HashMap。
1738 |
1739 | 这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量,负载因子是哈希表在其容量增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的填充程度越高,反之越小。
1740 |
1741 | **2. size为什么必须是2的整数次幂**
1742 | 这是为了服务key映射到index的hash算法的,公式 index=hashcode(key)&(length-1)。HashMap中数组的size必须是2的幂,是为了将key的hash值均匀的分布在数组的索引上。HashMap中使用indexFor方法来计算key所在的数组的索引,实现逻辑为key的hash值与数组长度值减一进行与运算,代码如下:
1743 |
1744 | ```java
1745 | static int indexFor(int h, int length) {
1746 | return h & (length - 1);
1747 | }
1748 | ```
1749 |
1750 | **3. HashMap的key为什么一般用字符串比较多,能用其他对象,或者自定义的对象嘛?**
1751 | 能用其他对象,必须是不可变的,但是自实现的类必须重写equals()和hashCode()方法,否则会调用默认的Object类的对应方法。
1752 |
1753 | **4. HashMap的key和value都能为null嘛?如果key为null,那么它是怎么样查找值的?**
1754 | 如果key为null,则直接从哈希表的第一个位置table[0]对应的链表上查找,由putForNullKey()实现,记住,key为null的键值对永远都放在以table[0]为头节点的链表中。
1755 |
1756 | **5. 使用HashMap时一般使用什么类型的元素作为Key?**
1757 | 一般是String、Integer,这些类是不可变的,并且这些类已经规范的复写了hashCode以及equals方法,作为不可变类天生是线程安全的,而且可以很好的优化比如可以缓存hash值,避免重复计算等等。
1758 |
1759 | **6. HashTable和HashMap的区别有哪些?**
1760 | 都实现了Map接口,主要区别在于:线程安全性,同步以及性能。
1761 |
1762 | - HashMap是非线程安全的,效率肯定高于线程安全的HashTable
1763 | - HashMap允许null作为一个entry的key或者value,而HashTable不允许
1764 | - HashMap把HashTable的contains方法去掉了,改成了containsVaule和containsKey
1765 | - HashTable和HashMap扩容的方法不一样,HashTable中的hash数组默认大小是11,扩容方式是old x 2+1,而HashMap中hash数组的默认大小是16,而且一定是2的指数,扩容时old x 2
1766 |
1767 |
1768 |
1769 | ## Java 部分(四)线程、多线程和线程池
1770 |
1771 | ### 1. 开启线程的三种方式?
1772 |
1773 | #### 继承Thread类创建线程类
1774 |
1775 | 1. 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run方法称为执行体。
1776 | 2. 创建Thread子类的实例,即创建了线程对象。
1777 | 3. 调用线程对象的start方法来启动该线程。
1778 |
1779 | ```java
1780 | public class FirstThreadTest extends Thread {
1781 | int i = 0;
1782 |
1783 | //重写run方法,run方法的方法体就是现场执行体
1784 | public void run() {
1785 | for (; i < 100; i++) {
1786 | System.out.println(getName() + " " + i);
1787 |
1788 | }
1789 | }
1790 |
1791 | public static void main(String[] args) {
1792 | for (int i = 0; i < 100; i++) {
1793 | System.out.println(Thread.currentThread().getName() + " : " + i);
1794 | if (i == 20) {
1795 | new FirstThreadTest().start();
1796 | new FirstThreadTest().start();
1797 | }
1798 | }
1799 | }
1800 |
1801 | }
1802 | ```
1803 |
1804 | #### 通过Runnable接口创建线程类
1805 |
1806 | 1. 定义runnable接口的实现类,并重写该接口的run方法,该run方法的方法体同样是该线程的线程执行体。
1807 | 2. 创建Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
1808 | 3. 调用线程对象的start方法来开启该线程。
1809 |
1810 | ```java
1811 | public class RunnableThreadTest implements Runnable {
1812 | private int i;
1813 |
1814 | public void run() {
1815 | for (i = 0; i < 100; i++) {
1816 | System.out.println(Thread.currentThread().getName() + " " + i);
1817 | }
1818 | }
1819 |
1820 | public static void main(String[] args) {
1821 | for (int i = 0; i < 100; i++) {
1822 | System.out.println(Thread.currentThread().getName() + " " + i);
1823 | if (i == 20) {
1824 | RunnableThreadTest rtt = new RunnableThreadTest();
1825 | new Thread(rtt, "新线程1").start();
1826 | new Thread(rtt, "新线程2").start();
1827 | }
1828 | }
1829 |
1830 | }
1831 |
1832 | }
1833 | ```
1834 |
1835 | #### 通过Callable和Future创建线程
1836 |
1837 | 1. 创建Callable接口的实现类,并实现call方法,该call方法将作为线程执行体,并且有返回值。
1838 | 2. 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call方法的返回值。
1839 | 3. 使用FutureTask对象作为Thread对象的target创建并启动新线程。
1840 | 4. 调用FutureTask对象的get方法来获取子线程执行结束后的返回值,调用get方法会阻塞线程。
1841 |
1842 | ```java
1843 | public class CallableThreadTest implements Callable {
1844 |
1845 | public static void main(String[] args) {
1846 | CallableThreadTest ctt = new CallableThreadTest();
1847 | FutureTask ft = new FutureTask<>(ctt);
1848 | for (int i = 0; i < 100; i++) {
1849 | System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i);
1850 | if (i == 20) {
1851 | new Thread(ft, "有返回值的线程").start();
1852 | }
1853 | }
1854 | try {
1855 | System.out.println("子线程的返回值:" + ft.get());
1856 | } catch (InterruptedException e) {
1857 | e.printStackTrace();
1858 | } catch (ExecutionException e) {
1859 | e.printStackTrace();
1860 | }
1861 | }
1862 |
1863 | @Override
1864 | public Integer call() throws Exception {
1865 | int i = 0;
1866 | for (; i < 100; i++) {
1867 | System.out.println(Thread.currentThread().getName() + " " + i);
1868 | }
1869 | return i;
1870 | }
1871 | }
1872 | ```
1873 |
1874 | #### 创建线程的三种方式的对比
1875 |
1876 | **采用实现Runnable、Callable接口的方式创建多线程时:**
1877 |
1878 | 优势:
1879 |
1880 | 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现面向对象的思想。
1881 |
1882 | 劣势:
1883 |
1884 | 稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
1885 |
1886 | **使用继承Thread类的方式创建多线程时:**
1887 |
1888 | 优势:
1889 |
1890 | 编写简单,如果需要访问当前线程,则不需使用Thread.currentThread方法,直接使用this即可获得当前线程。
1891 |
1892 | 劣势:
1893 |
1894 | 线程类已经继承了Thread类,所以不能在继承其他父类。
1895 |
1896 | ### 2. 线程和进程的区别?
1897 |
1898 |
1899 |
1900 | ### 3. 为什么要有线程,而不是仅仅用进程?
1901 |
1902 | 线程增加进程的并发度,线程能更有效的利用多处理器和多内核。
1903 |
1904 | ### 4. run() 和 start() 方法的区别?
1905 |
1906 | 主要区别在于当程序调用start方法一个新线程将会被创建,并且在run方法中的代码将会在新线程上运行,然而在你直接调用run方法的时候,程序并不会创建新线程,run方法内部的代码将在当前线程上运行。
1907 |
1908 | 另外一个区别在于,一旦一个线程被启动,你不能重复调用该Thread对象的start方法,调用已经启动线程的start方法将会报IIIegalStateException异常,而重复调用run方法是没有问题的。
1909 |
1910 | ### 5. 如何控制某个方法允许并发访问线程的个数?
1911 |
1912 | ```java
1913 | Semaphore semaphore = new Semaphore(5);//线程run中只有5个线程可并发访问
1914 | semaphore.acquire();//申请一个信号
1915 | semaphore.release();//释放一个信号
1916 | ```
1917 |
1918 | ### 6. 在 Java 中 wait() 和 sleep() 方法的区别
1919 |
1920 |
1921 |
1922 | ### 7. 谈谈 wait/notify 关键字的理解
1923 |
1924 | 都是本地final方法,调用之前持有对象锁。
1925 |
1926 | wait,线程进入挂起状态,释放对象锁。
1927 |
1928 | notify,通知唤醒wait队列中的线程(某个)
1929 |
1930 | ### 8. 什么导致线程阻塞?
1931 |
1932 | Thread.sleep t.join 等待输入
1933 |
1934 | ### 9. 线程如何关闭?
1935 |
1936 |
1937 |
1938 | ### 10. 讲一下 Java 中的同步的方法
1939 |
1940 |
1941 |
1942 | ### 11. 数据一致性如何保证?
1943 |
1944 |
1945 |
1946 | ### 12. 如何保证线程安全?
1947 |
1948 |
1949 |
1950 | ### 13. 如何实现线程同步?
1951 |
1952 |
1953 |
1954 | ### 14. 两个进程同时要求读写,能不能实现?如何防止进程同步?
1955 |
1956 |
1957 |
1958 | ### 15. 线程间操作 List
1959 |
1960 |
1961 |
1962 | ### 16. Java 中对象的生命周期
1963 |
1964 |
1965 |
1966 | ### 17. 说说线程池
1967 |
1968 | **线程池的好处**
1969 |
1970 | 1. 线程池的重用
1971 |
1972 | 线程的创建和销毁的开销是巨大的,而通过线程池的重用大大减少了这些不必要的开销,当然既然少了那么多消耗内存的开销,其线程执行速度也是突飞猛进的提升。
1973 |
1974 | 2. 控制线程池的并发数
1975 |
1976 | 控制线程池的并发数可以有效地避免大量的线程争夺CPU资源而造成阻塞。
1977 |
1978 | 3. 线程池可以对线程进行管理
1979 |
1980 | 线程池可以提供定时、定期、单线程、并发数控制等功能。
1981 |
1982 | **线程池的详解**
1983 |
1984 | 1. ThreadPoolExecutor
1985 |
1986 | ```java
1987 |
1988 | public ThreadPoolExecutor(int corePoolSize,
1989 | int maximumPoolSize,
1990 | long keepAliveTime,
1991 | TimeUnit unit,
1992 | BlockingQueue workQueue,
1993 | ThreadFactory threadFactory,
1994 | RejectedExecutionHandler handler)
1995 | ```
1996 |
1997 | 这里是七个参数(更多的是用五个参数的构造方法)。
1998 |
1999 | **corePoolSize**:线程池中核心线程的数量。
2000 |
2001 | **maxinumPoolSize**:线程池中最大的线程数量。
2002 |
2003 | **keepAliveTime**:非核心线程的超时时长,当系统中非核心线程闲置时间超过keepAliveTime之后,则会被回收。如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长。
2004 |
2005 | **unit**:第三个参数的单位,有毫秒、秒、分等等。
2006 |
2007 | **workQueue**:线程池中的任务队列,该队列主要用来存储已经被提交但尚未执行的任务。存储在这里的任务是由ThreadPoolExecutor的execute方法提交来的。
2008 |
2009 | threadFactory:为线程池提供创建新线程的功能,这个我们一般使用默认即可。
2010 |
2011 | **handler**:拒绝策略,当线程无法执行新任务(一般是由于线程池中的线程数量已经达到最大数或者线程池关闭导致的)。默认情况下,当线程池无法处理新线程时,会抛出一个RejectedExecutionException。
2012 |
2013 | 有以下:
2014 |
2015 | * 当currentSize < corePoolSize 时,直接启动一个核心线程并执行任务
2016 | * 当currentSize >= corePoolSize,并且 workQueue 未满时,添加进来的任务会被安排到workQueue中等待执行
2017 | * 当workQueue已满,但是currentSize < maxinumPoolSize 时,会立即开启一个非核心线程来执行任务
2018 | * 当 currentSize >= corePoolSize、workQueue已满,并且currentSize > maxinumPoolSize 时,调用handler默认会抛出异常
2019 |
2020 | 2. 其他线程池
2021 |
2022 | 1. FixedThreadPool
2023 |
2024 | 有固定数量线程的线程池,其中corePoolSize = maxinumPoolSize,且keepAliveTime为0,适合线程稳定的场所。
2025 |
2026 | 2. SingleThreadPool
2027 |
2028 | corePoolSize = maxinumPoolSzie = 1 且 keepAliveTime 为0,适合线程同步操作的场所。
2029 |
2030 | 3. CachedThreadPool
2031 |
2032 | corePoolSize = 0,maximunPoolSize = Integer.MAX_VALUE(2的32次方-1)。
2033 |
2034 | 4. ScheduledThreadPool
2035 |
2036 | 是一个具有定时定期执行任务功能的线程池。
2037 |
2038 | ### 18. 并发编程面临的挑战
2039 |
2040 | 1. 上下文切换
2041 | 2. 死锁
2042 | 3. 资源限制
2043 |
2044 | 参考:[并发编程面临的挑战](https://www.jianshu.com/p/f3e69850addf)
2045 |
2046 | ## Android 部分(一)基础知识点
2047 |
2048 | ### 1. 四大组件是什么?
2049 |
2050 | 四大组件:Activity、Service、BroadcastReceiver、ContentProvider。
2051 |
2052 | ### 2. 四大组件的生命周期和简单用法
2053 |
2054 | ### **Activity:**
2055 |
2056 | 典型的生命周期好像没什么可说的,主要说一下特殊情况下的生命周期。
2057 |
2058 | 1. 横竖屏的切换
2059 |
2060 | 在横竖屏切换的过程中,会发生Activity被销毁并被重建的过程。
2061 |
2062 | 在了解这种情况下的生命周期,首先应该了解这两个回调:onSaveInstance State 和 onRestoreInstance State。
2063 |
2064 | 在Activity由于异常情况下终止时,系统会调用 onSaveInstanceState 来保存当前 Activity 的状态。这个方法的调用是在onStop之前,它和onPause没有既定的时序关系,该方法只有在Activity被异常终止的情况下调用。当异常终止的Activity被重建之后,系统会调用onRestoreInstanceState,并且把Activity销毁时onSaveInstanceState方法所保存的Bundle对象参数同时传递给onRestoreInstanceState和onCreate方法。因为,可以通过onRestoreInstanceState方法来恢复Activity的状态,该方法的调用时机是在onStart之后。其中,onCreate和onRestoreInstanceState方法来恢复Activity状态的区别:onRestoreInstanceState回调则表明其中Bundle对象非空,不用加非空判断,而onCreate需要非空判断,建议使用onRestoreInstanceState。
2065 |
2066 | 
2067 |
2068 | 横竖屏切换的生命周期:onPause() --> onSaveInstanceState() --> onStop() --> onDestory() --> onCreate() --> onStart() --> onRestoreInstanceState() --> onResume()
2069 |
2070 | 可以通过在AndroidManifest文件的Activity中指定如下属性:
2071 |
2072 | android:configChanges = "orientation| screenSize"
2073 |
2074 | 来避免横竖屏切换时,Activity的销毁和重建,而是回调了下面的方法:
2075 |
2076 | ```java
2077 | @Override
2078 | public void onConfigurationChanged(Configuration newConfig) {
2079 | super.onConfigurationChanged(newConfig);
2080 | }
2081 | ```
2082 |
2083 | 2. 资源内存不足导致优先级低的Activity被杀死
2084 |
2085 | 优先级:前台Activity > 可见但非前台Activity > 后台Activity
2086 |
2087 | **启动模式:**
2088 |
2089 | Android 提供了四种Activity启动方式:
2090 |
2091 | 标准模式:standard
2092 |
2093 | 栈顶复用模式:singleTop
2094 |
2095 | 栈内复用模式:singleTask
2096 |
2097 | 单例模式:singleInstance
2098 |
2099 | 1. 标准模式 standard
2100 |
2101 | 每启动一次Activity,就会创建一个新的Activity实例并置于栈顶。谁启动了这个Activity,那么这个Activity就运行在启动它的那个Activity所在的栈中。
2102 |
2103 | 特殊情况下,如果在Service或Application中启动一个Activity,其并没有所谓的任务栈,可以使用标记位Flag来解决。解决办法:为待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,创建一个新栈。
2104 |
2105 | 2. 栈顶复用模式 singleTop
2106 |
2107 | 如果需要新建的Activity位于任务栈栈顶,那么此Activity的实例就不会重建,而是复用栈顶的实例。并回调:
2108 |
2109 | ```java
2110 | @Override
2111 | protected void onNewIntent(Intent intent) {
2112 | super.onNewIntent(intent);
2113 | }
2114 | ```
2115 |
2116 | 由于不会重建一个Activity实例,则不会回调其他生命周期方法。
2117 |
2118 | 应用场景:在通知栏点击收到的通知,然后需要启动一个Activity,这个Activity就可以用singleTop,否则每次点击都会新建一个Activity。
2119 |
2120 | 3. 栈内复用模式 singleTask
2121 |
2122 | 该模式是一种单例模式,即一个栈内只有一个该Activity实例。该模式,可以通过在AndroidManifest文件的Activity中指定该Activity需要加载到哪个栈中,即singleTask的Activity可以指定想要加载的目标栈。singleTask和taskAffinity配合使用,指定开启的Activity加入到哪个栈中。
2123 |
2124 | ```xml
2125 |
2129 |
2130 | ```
2131 |
2132 | 关于taskAffinity的值:每个Activity都有taskAffinify属性,这个属性指出了它希望进入的Task。如果一个Activity没有显式的指明该Activity的taskAffinity,那么它的这个属性就等于Application指明的taskAffinity,如果Application也没有指明,那么该taskAffinity的值就等于包名。
2133 |
2134 | 执行逻辑:
2135 |
2136 | 在这种模式下,如果Activity指定的栈不存在,则创建一个栈,并把创建的Activity压入栈内。如果Activity指定的栈存在,如果其中没有该Activity实例,则会创建Activity并压入栈顶,如果其中有该Activity实例,则把该Activity实例之上的Activity杀死清除出战,重用并让该Activity实例处在栈顶,然后调用onNewIntent()方法。
2137 |
2138 | 应用场景:
2139 |
2140 | 在大多数App的主页,对于大部分应用,当我们在主界面点击返回按钮都是退出应用,那么当我们第一次进入主界面之后,主界面位于栈底,以后不管我们打开了多少个Activity,只要我们再次回到主界面,都应该使用将主界面Activity上所有的Activity移除的方式来让主界面Activity处于栈顶,而不是往栈顶新加一个主界面Activity的实例,通过这种方式能够保证退出应用时所有的Activity都能被销毁。
2141 |
2142 | 4. 单例模式 singleInstance
2143 |
2144 | 作为栈内复用的加强版,打开该Activity时,直接创建一个新的任务栈,并创建该Activity实例放入栈中。一旦该模式的Activity实例已经存在于某个栈中,任何应用在激活该Activity时都会重用该栈中的实例。
2145 |
2146 | 应用场景:呼叫来电界面
2147 |
2148 | **特殊情况:前台栈和后台栈的交互**
2149 |
2150 | 
2151 |
2152 | 
2153 |
2154 | 调用SingleTask模式的后台任务栈的Activity,会把整个栈的Activity压入当前栈的栈顶。singleTask会具有clearTop特性,会把之上的栈内Activity清除。
2155 |
2156 | **Activity的Flags:**
2157 |
2158 | Activity的Flags很多,这里介绍集中常用的,用于设定Activity的启动模式,可以在启动Activity时,通过Intent.addFlags()方法设置。
2159 |
2160 | 1. FLAG_ACTIVITY_NEW_TASK 即 singleTask
2161 | 2. FLAG_ACTIVITY_SINGLE_TOP 即 singleTop
2162 | 3. FLAG_ACTIVITY_CLEAR_TOP 当他启动时,在同一个任务栈中所有位于它之上的Activity都要出栈。如果和singleTask模式一起出现,若被启动的Activity已经存在栈中,则清除其之上的Activity,并调用该Activity的onNewIntent方法。如果被启动的Activity采用standard模式,那么该Activity连同之上的所有Activity出栈,然后创建新的Activity实例并压入栈中。
2163 |
2164 |
2165 | **Activity的启动过程**
2166 |
2167 | 
2168 |
2169 | **应用启动过程**
2170 |
2171 | 1. Launcher通过Binder进程间通信机制通知AMS,它要启动一个Activity
2172 | 2. AMS通过Binder进程间通信机制通知Launcher进入Paused状态
2173 | 3. Launcher通过Binder进程间通信机制通知AMS,它已经准备就绪进入Paused状态,于是AMS就创建一个新的线程,用来启动一个ActivityThread实例,即将要启动的Activity就是在这个ActivityThread实例中运行
2174 | 4. ActivityThread通过Binder进程间通信机制将一个ApplicationThread类型的Binder对象传递给AMS,以便以后AMS能够通过这个Binder对象和它进行通信
2175 | 5. AMS通过Binde进程间通信机制通知ActivityThread,现在一切准备就绪,它可以真正执行Activity的启动操作了
2176 |
2177 |
2178 | ### Service
2179 |
2180 | 是Android中实现程序后台运行的解决方案,它非常适用于去执行那些不需要和用户交互而且还要长期运行的任务。Service默认并不会运行在子线程中,它也不运行在一个独立的进程中,它同样执行在UI线程中,因此,不要在Service中执行耗时的操作,因此,不要在Service中执行耗时的操作,除非你在Service中创建了子线程来完成耗时操作。
2181 |
2182 | #### Service种类划分
2183 |
2184 | 按运行地点分类:
2185 |
2186 | | 类别 | 区别 | 优点 | 缺点 | 应用 |
2187 | | -------------------- | ---------- | ---------------------------------------- | ----------------------------------- | ------------------------------ |
2188 | | 本地服务(Local Service) | 该服务依附在主进程中 | 服务依附于主进程而不是独立的进程,这样在一定程度上节约了资源,因为Local服务是在同一个进程,所以不需要IPC和AIDL。相应bindService会方便很多。 | 主进程被kill后,服务便会终止 | 如:音乐播放等不需要常驻的服务 |
2189 | | 远程服务(Remote Service) | | 服务为独立的进程,对应进程名格式为所在包名加上你指定的 android:process 字符串。由于是独立的进程,因此在Activity所在进程被kill的时候,该服务依然在运行,不受其他进程影响,有利于为多个进程提供服务具有较高的灵活性。 | 该服务是独立的进程,会占用一定的资源,并且使用AIDL进行IPC稍麻烦 | 一些提供系统服务的Service,这种Service是常住的 |
2190 |
2191 | 安运行类型分类:
2192 |
2193 | | 类别 | 区别 | 应用 |
2194 | | ---- | ---------------------------------------- | ---------------------------------------- |
2195 | | 前台服务 | 会在通知栏显示onGoing的Notification | 当服务被终止的时候,通知栏的Notification也会消失,这样对于用户有一定的通知作用。常见的如音乐播放服务 |
2196 | | 后台服务 | 默认的服务即为后台服务,即不会在通知一栏显示onGoing的Notification | 当服务被终止的时候,用户是看不到效果的。某些不需要运行或者终止提供的服务,如天气更新、日期同步等等 |
2197 |
2198 | 按使用方式分类:
2199 |
2200 | | 类别 | 区别 |
2201 | | --------------------------------- | ---------------------------------------- |
2202 | | startService启动的服务 | 主要用于启动一个服务执行后台任务,不进行通信,停止服务使用stopService |
2203 | | bindService启动的服务 | 启动的服务要进行通信,停止服务使用unbindService |
2204 | | 同时使用startService、bindService启动的服务 | 停止服务应同时使用stopService与unbindService |
2205 |
2206 | #### Service生命周期
2207 |
2208 | startService() --> onCreate() --> onStartCommand() --> Service running --> onDestory()
2209 |
2210 | bindService() --> onCreate() --> onBind() --> Service running --> onUnbind() --> onDestory()
2211 |
2212 | **onCreate():**
2213 |
2214 | 系统在Service第一次创建时执行此方法,来执行**只运行一次**的初始化工作,如果service已经运行,这个方法不会调用。
2215 |
2216 | **onStartCommand():**
2217 |
2218 | 每次客户端调用startService()方法启动该Service都会回调该方法(**多次调用**),一旦这个方法执行,service就启动并且在后台长期运行,通过调用stopSelf()或stopService()来停止服务。
2219 |
2220 | **onBind():**
2221 |
2222 | 当组件调用bindService()想要绑定到service时,系统调用此方法(**一次调用**),一旦绑定后,下次在调用bindService()不会回调该方法。在你的实现中,你必须提供一个返回一个IBinder来使客户端能够使用它与service通讯,你必须总是实现这个方法,但是如果你不允许绑定,那么你应返回null
2223 |
2224 | **onUnbind():**
2225 |
2226 | 当前组件调用unbindService(),想要解除与service的绑定时系统调用此方法(**一次调用**,一旦解除绑定后,下次再调用unbindService()会抛异常)
2227 |
2228 | **onDestory():**
2229 |
2230 | 系统在service不在被使用并且要销毁的时候调用此方法(**一次调用**)。service应在此方法中释放资源,比如线程,已注册的监听器、接收器等等。
2231 |
2232 | #### 三种情况下Service的生命周期
2233 |
2234 | 1. startService / stopService
2235 |
2236 | 生命周期:onCreate --> onStartCommand --> onDestory
2237 |
2238 | 如果一个Service被某个Activity调用Context.startService 方法启动,那么不管是否有Activity使用bindService绑定或unbindService解除绑定到该Service,该Service都在后台运行,直到被调用stopService,或自身的stopSelf方法。当然如果系统资源不足,Android系统也可能结束服务,还有一种方法可以关闭服务,在设置中,通过应用 --> 找到自己应用 --> 停止。
2239 |
2240 | 注意:
2241 |
2242 | 第一次startService会触发onCreate和onStartCommand,以后在服务运行过程中,每次startService都只会触发onStartCommand
2243 |
2244 | 不论startService多少次,stopService一次就会停止服务
2245 |
2246 | 2. bindService / unbindService
2247 |
2248 | 生命周期:onCreate --> onBind --> onUnbind --> onDestory
2249 |
2250 | 如果一个Service在某个Activity中被调用bindService方法启动,不论bindService被调用几次,Service的onCreate方法只会执行一次,同时onStartCommand方法始终不会调用。
2251 |
2252 | 当建立连接后,Service会一直运行,除非调用unbindService来解除绑定、断开连接或调用该Service的Context不存在了(如Activity被finish --- **即通过bindService启动的Service的生命周期依附于启动它的Context**),系统会在这时候自动停止该Service。
2253 |
2254 | 注意:
2255 |
2256 | 第一次bindService会触发onCreate和inBind,以后在服务运行过程中,每次bindService都不会触发任何回调
2257 |
2258 | 3. 混合型
2259 |
2260 | 当一个Service再被启动(startService)的同时又被绑定(bindService),该Service将会一直在后台运行,不管调用几次,onCreate方法始终只会调用一次,onStartCommand的调用次数与startService调用的次数一致(使用bindService方法不会调用onStartCommand)。同时,调用unBindService将不会停止Service,必须调用stopService或Service自身的stopSelf来停止服务。
2261 |
2262 |
2263 | #### 三种情况下的应用场景
2264 |
2265 | 如果你只是想启动一个后台服务长期进行某项任务,那么使用startService便可以了。
2266 |
2267 | 如果你想与正在运行的Service取的联系,那么有两种方法,一种是使用broadcast,另外是使用bindService。前者的缺点是如果交流较为频繁,容易造成性能上的问题,并且BroadcastReceiver本身执行代码的时间是很短的(也许执行到一半,后面的代码便不会执行),而后者则没有这些问题,因此我们肯定选择使用bindService(这个时候便同时使用了startService和bindService了,这在Activity中更新Service的某些运行状态是相当有用的)
2268 |
2269 | 如果你的服务只是公开了一个远程接口,供连接上的客户端(Android的Service是C/S架构)远程调用执行方法。这个时候你可以不让服务一开始就运行,而只用bindService,这样在第一次bindService的时候才会创建服务的实例运行它,这会节约很多系统资源,特别是如果你的服务是Remote Service,那么该效果会越明显。
2270 |
2271 |
2272 |
2273 | ### BroadcastReceiver
2274 |
2275 | 广播接收器
2276 |
2277 | 作用:用于监听 / 接收 应用发出的广播消息,并作出响应
2278 |
2279 | 应用场景:
2280 |
2281 | 1. 不同组件之间的通信(包括应用内 / 不同应用之间)
2282 | 2. 与Android系统在特定情况下的通信,如当电话呼入时,网络可用时
2283 | 3. 多线程通信
2284 |
2285 | #### 实现原理
2286 |
2287 | * 使用了观察者模式:基于消息的发布/订阅事件模型。
2288 |
2289 | * 模型中有三个角色:消息订阅者(广播接收者)、消息发布者(广播发布者)和消息中心(AMS,即Activity Manager Service)
2290 |
2291 | 
2292 |
2293 | * 原理描述
2294 |
2295 | 1. 广播接收者通过Binder机制在AMS注册
2296 | 2. 广播发送者通过Binder机制向AMS发送广播
2297 | 3. AMS根据广播发送者要求,在已注册列表中,寻找合适的广播接收者,寻找依据:IntentFilter / Permission
2298 | 4. AMS将广播发送到合适的广播接收者相应的消息循环队列
2299 | 5. 广播接收者通过消息循环拿到此广播,并回调onReceive()
2300 |
2301 | 注意:广播发送者和广播接收者的执行是异步的,发出去的广播不会关心有没有接收者接收,也不确定接收者何时能接受到。
2302 |
2303 | #### 具体使用
2304 |
2305 | 
2306 |
2307 | #### 自定义广播接收者BroadcastReceiver
2308 |
2309 | * 继承至BroadcastReceiver基类
2310 |
2311 | * 重写onReceiver()方法
2312 |
2313 | 广播接收器收到相应广播后,会自动回调onReceiver()方法;一般情况下,onReceiver方法会涉及与其他组件之间的交互,如发送Notification、启动service等等;默认情况下,广播接收器运行在UI线程,因此onReceiver方法不能执行耗时操作,否则可能ANR
2314 |
2315 | #### 广播接收器注册
2316 |
2317 | 注册方式分为两种:静态注册和动态注册。
2318 |
2319 | **静态注册:**
2320 |
2321 | 在AndroidManifest.xml里通过标签声明:
2322 |
2323 | ```xml
2324 |
2339 |
2340 | //用于指定此广播接收器将接收的广播类型
2341 | //本示例中给出的是用于接收网络状态改变时发出的广播
2342 |
2343 |
2344 |
2345 |
2346 | ```
2347 |
2348 | **动态注册:**
2349 |
2350 | 在代码中通过调用Context的registerReceiver()方法进行动态注册BroadcastReceiver:
2351 |
2352 | ```java
2353 | @Override
2354 | protected void onResume() {
2355 | super.onResume();
2356 |
2357 | //实例化BroadcastReceiver子类 & IntentFilter
2358 | mBroadcastReceiver mBroadcastReceiver = new mBroadcastReceiver();
2359 | IntentFilter intentFilter = new IntentFilter();
2360 |
2361 | //设置接收广播的类型
2362 | intentFilter.addAction(android.net.conn.CONNECTIVITY_CHANGE);
2363 |
2364 | //调用Context的registerReceiver()方法进行动态注册
2365 | registerReceiver(mBroadcastReceiver, intentFilter);
2366 | }
2367 |
2368 |
2369 | //注册广播后,要在相应位置记得销毁广播
2370 | //即在onPause() 中unregisterReceiver(mBroadcastReceiver)
2371 | //当此Activity实例化时,会动态将MyBroadcastReceiver注册到系统中
2372 | //当此Activity销毁时,动态注册的MyBroadcastReceiver将不再接收到相应的广播。
2373 | @Override
2374 | protected void onPause() {
2375 | super.onPause();
2376 | //销毁在onResume()方法中的广播
2377 | unregisterReceiver(mBroadcastReceiver);
2378 | }
2379 | ```
2380 |
2381 | 注意:
2382 |
2383 | 动态广播最好在Activity的onResume()注册,onPause()注销,否则会导致内存泄漏,当然,重复注册和重复注销也不允许。
2384 |
2385 | **两种注册方式的区别:**
2386 |
2387 | | 注册方式 | 特点 | 应用场景 |
2388 | | ----------- | ---------------------------------------- | ---------- |
2389 | | 静态注册(常驻广播) | 常驻,不受任何组件的生命周期影响(应用程序关闭后,如果有信息广播来,程序依旧会被系统调用)缺点:耗电、占内存 | 需要时刻监听广播 |
2390 | | 动态注册(非常驻广播) | 非常驻,灵活,跟随组件的生命周期变化(组件结束 = 广播结束,在组件结束前,必须移除广播接收器) | 需要特定时刻监听广播 |
2391 |
2392 | #### 广播发送者向AMS发送广播
2393 |
2394 | ##### 广播的发送
2395 |
2396 | * 广播是用意图(Intent)标识
2397 | * 定义广播的本质:定义广播所具备的意图(Intent)
2398 | * 广播发送:广播发送者将此广播的意图通过sendBroadcast()方法发送出去
2399 |
2400 | ##### 广播的类型
2401 |
2402 | 广播的类型主要分为5类:
2403 |
2404 | * 普通广播(Normal Broadcast)
2405 | * 系统广播(System Broadcast)
2406 | * 有序广播(Ordered Broadcast)
2407 | * 粘性广播(Sticky Broadcast)
2408 | * App应用内广播(Local Broadcast)
2409 |
2410 | **普通广播:**
2411 |
2412 | 即开发者自身定义Intent的广播(最常用),发送广播使用如下:
2413 |
2414 | ```java
2415 | Intent intent = new Intent();
2416 | //对应BroadcastReceiver中intentFilter的action
2417 | intent.setAction(BROADCAST_ACTION);
2418 | //发送广播
2419 | sendBroadcast(intent);
2420 | ```
2421 |
2422 | * 若被注册了的广播接收者中注册时IntentFilter的action与上述匹配,则会接收此广播(即进行回调onReceiver()),如下mBroadcastReceiver则会接收上述广播:
2423 |
2424 | ```xml
2425 |
2428 | //用于接收网络状态改变时发出的广播
2429 |
2430 |
2431 |
2432 |
2433 | ```
2434 |
2435 | * 若发送广播有相应权限,那么广播接收者也需要相应权限
2436 |
2437 | ##### 系统广播
2438 |
2439 | * Android中内置了很多系统广播:只要涉及到手机的基本操作(如开关机、网络状态变化、拍照等等),都会发出相应的广播
2440 |
2441 | * 每个广播都有特定的IntentFilter(包括具体的action)Android系统广播action如下:
2442 |
2443 | | 系统操作 | action |
2444 | | ---------------------------------------- | ------------------------------------ |
2445 | | 监听网络变化 | android.net.conn.CONNECTIVITY_CHANGE |
2446 | | 关闭或打开飞行模式 | Intent.ACTION_AIRPLANE_MODE_CHANGED |
2447 | | 充电时或电量发生变化 | Intent.ACTION_BATTERY_CHANGED |
2448 | | 电池电量低 | Intent.ACTION_BATTERY_LOW |
2449 | | 电池电量充足(即从电量低变化到饱满时会发出广播) | Intent.ACTION_BATTERY_OKAY |
2450 | | 系统启动完成后(仅广播一次) | Intent.ACTION_BOOT_COMPLETED |
2451 | | 按下照相时的拍照按键(硬件按键)时 | Intent.ACTION_CAMERA_BUTTON |
2452 | | 屏幕锁屏 | Intent.ACTION_CLOSE_SYSTEM_DIALOGS |
2453 | | 设备当前设置被改变时(设备语言、设备方向等) | Intent.ACTION_CONFIGURATION_CHANGED |
2454 | | 插入耳机时 | Intent.ACTION_HEADSET_PLUG |
2455 | | 未正确移除SD卡但已取出来时(正确移除方法:设置SD卡和设备内存--卸载SD卡) | Intent.ACTION_MEDIA_BAD_REMOVAL |
2456 | | 插入外部存储装置(如SD卡) | Intent.ACTION_MEDIA_CHECKING |
2457 | | 成功安装APK | Intent.ACTION_PACKAGE_ADDED |
2458 | | 成功删除APK | Intent.ACTION_PACKAGE_REMOVE |
2459 | | 重启设备 | Intent.ACTION_REBOOT |
2460 | | 屏幕被关闭 | Intent.ACTION_SCREEN_OFF |
2461 | | 屏幕被打开 | Intent.ACTION_SCREENT_ON |
2462 | | 关闭系统时 | Intent.ACTION_SHUTDOWN |
2463 | | 重启设备 | Intent.ACTION_REBOOT |
2464 |
2465 | 注:当使用系统广播时,只需要在注册广播接收者时定义相关的action即可,并不需要手动发送广播,当系统有相关操作时会自动进行系统广播
2466 |
2467 | ##### 有序广播
2468 |
2469 | * 定义:发送出去的广播被接收者按照先后顺序接收,有序是针对广播接收者而言的
2470 | * 广播接收者接收广播的顺序规则(同时面向静态和动态注册的广播接收者)
2471 | * 按照Priority属性值从大到小排序
2472 | * Priority属性相同者,动态注册的广播优先
2473 | * 特点:
2474 | * 接收广播按顺序接收
2475 | * 先接收的广播接收者可以对广播进行截断,即后接收的广播接收者不在接收到此广播
2476 | * 先接收的广播接收者可以对广播进行修改,那么后接收的广播接收者将接收到被修改后的广播
2477 | * 具体使用有序广播的使用过程和普通广播非常类似,差异仅在于广播的发送方式:sendOrderedBroadcast(intent);
2478 |
2479 | ##### APP应用内广播
2480 |
2481 | * Android中的广播可以跨App直接通信(exported对于有intent-filter情况下默认为true)
2482 |
2483 | * 冲突可能出现的问题:
2484 | * 其他App针对性发出与当前App intent-filter 相匹配的广播,由此导致当前App不断接收广播并处理;
2485 | * 其他App注册与当前App一致的intent-filter用于接收广播,获取广播具体信息。即会出现安全性&效率性的问题
2486 |
2487 | * 解决方案 使用App应用内广播(Local Broadcast)
2488 | * App应用内广播可以理解为一种局部广播,广播的发送者和接收者都同属于一个App
2489 | * 相比于全局广播(普通广播),App应用内广播优势体现在:安全性高 & 效率高
2490 |
2491 | * 具体使用1 将全局广播设置成局部广播
2492 | * 注册广播时将exported属性设置为false,使得非本App内部发出的此广播不被接受
2493 | * 在广播发送和接收时,增设相应权限permission,用于权限验证
2494 | * 发送广播时指定该广播接收器所在的包名,此广播将只会发送到此包中的App内与之相匹配的有效广播接收器中,通过intent.setPackage(packageName)指定包名
2495 |
2496 | * 具体使用2 使用封装好的LocalBroadcastManager类
2497 |
2498 | 使用方式上与全局广播几乎相同,只是注册 / 取消注册广播接收器和发送广播时将参数的context变成了LocalBroadcastManager的单一实例
2499 |
2500 | 注:对于LocalBroadcastManager方式发送的应用内广播,只能通过LocalBroadcastManager动态注册,不能静态注册。
2501 |
2502 | ```java
2503 | //注册应用内广播接收器
2504 | //步骤1:实例化BroadcastReceiver子类 & IntentFilter mBroadcastReceiver
2505 | mBroadcastReceiver = new mBroadcastReceiver();
2506 | IntentFilter intentFilter = new IntentFilter();
2507 |
2508 | //步骤2:实例化LocalBroadcastManager的实例
2509 | localBroadcastManager = LocalBroadcastManager.getInstance(this);
2510 |
2511 | //步骤3:设置接收广播的类型
2512 | intentFilter.addAction(android.net.conn.CONNECTIVITY_CHANGE);
2513 |
2514 | //步骤4:调用LocalBroadcastManager单一实例的registerReceiver()方法进行动态注册
2515 | localBroadcastManager.registerReceiver(mBroadcastReceiver, intentFilter);
2516 |
2517 | //取消注册应用内广播接收器
2518 | localBroadcastManager.unregisterReceiver(mBroadcastReceiver);
2519 |
2520 | //发送应用内广播
2521 | Intent intent = new Intent();
2522 | intent.setAction(BROADCAST_ACTION);
2523 | localBroadcastManager.sendBroadcast(intent);
2524 | ```
2525 |
2526 | ##### 粘性广播
2527 |
2528 | Android 5.0 & API 21中已经失效
2529 |
2530 |
2531 |
2532 | ### ContentProvider
2533 |
2534 | 内容提供者
2535 |
2536 | #### 作用
2537 |
2538 | 进程间进行数据交互&共享,即跨进程通信
2539 |
2540 | 
2541 |
2542 | #### 原理
2543 |
2544 | ContentProvider的底层采用Android中的Binder机制
2545 |
2546 | #### 具体使用
2547 |
2548 | 
2549 |
2550 | ##### 统一资源标识符(RUI)
2551 |
2552 | 作用:唯一标识ContentProvider & 其中的数据,外界进程通过URI找到对应的ContentProvider & 其中的数据,再进行数据操作
2553 |
2554 | 具体使用:
2555 |
2556 | URI分为系统预置 & 自定义,分别对应系统内置的数据(如通讯录、日程表等等)和自定义数据库。
2557 |
2558 | 
2559 |
2560 |
2561 |
2562 | ```java
2563 | // 设置URI
2564 | Uri uri = Uri.parse("content://com.carson.provider/User/1")
2565 | // 上述URI指向的资源是:名为 `com.carson.provider`的`ContentProvider` 中表名 为`User` 中的 `id`为1的数据
2566 |
2567 | // 特别注意:URI模式存在匹配通配符* & #
2568 |
2569 | // *:匹配任意长度的任何有效字符的字符串
2570 | // 以下的URI 表示 匹配provider的任何内容
2571 | content://com.example.app.provider/*
2572 | // #:匹配任意长度的数字字符的字符串
2573 | // 以下的URI 表示 匹配provider中的table表的所有行
2574 | content://com.example.app.provider/table/#
2575 | ```
2576 |
2577 | ##### MIME数据类型
2578 |
2579 | * MIME,即多功能Internet邮件扩充服务。它是一种多用途网际邮件扩充协议。MIME类型就是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。
2580 |
2581 | * 作用:指定某个扩展名的文件用某种应用程序来打开。
2582 |
2583 | * ContentProvider根据URI返回MIME类型
2584 |
2585 | ```
2586 | ContentProvider.getType(uri);
2587 | ```
2588 |
2589 | * MIME类型组成
2590 |
2591 | 每种MIME类型由两部分组成=类型+子类型
2592 |
2593 | MIME类型是一个包含两部分的字符串,例:
2594 |
2595 | text / html 类型为text 子类型为html
2596 |
2597 | text/css text/xml 等等
2598 |
2599 |
2600 |
2601 | ### 3. Context的理解?
2602 |
2603 | Android应用模型是基于组件的应用设计模式,组件的运行要有一个完整的Android工程环境。在这个工程环境下,Activity、Service等系统组件才能够正常工作,而这些组件并不能采用普通的Java对象创建方式,new一下就能创建实例了,而是要有它们各自的上下文环境,也就是Context,Context是维持Android程序中各组件能够正常工作的一个核心功能类。
2604 |
2605 | **如何生动形象的理解Context?**
2606 |
2607 | 一个Android程序可以理解为一部电影,Activity、Service、BroadcastReceiver和ContentProvider这四大组件就好比戏了的四个主角,它们是剧组(系统)一开始定好的,主角并不是大街上随便拉个人(new 一个对象)都能演的。有了演员当然也得有摄像机拍摄啊,它们必须通过镜头(Context)才能将戏传给观众,这也就正对应说四大组件必须工作在Context环境下。那么Button、TextView等等控件就相当于群演,显然没那么重用,随便一个路人甲都能演(可以new一个对象),但是它们也必须在面对镜头(工作在Context环境下),所以Button mButtom = new Button(context) 是可以的。
2608 |
2609 | **源码中的Context**
2610 |
2611 | ```java
2612 | public abstract class Context {
2613 | }
2614 | ```
2615 |
2616 | 它是一个纯抽象类,那就看看它的实现类。
2617 |
2618 | 
2619 |
2620 | 它有两个具体实现类:ContextImpl和ContextWrapper。
2621 |
2622 | 其中ContextWrapper类,是一个包装类而已,ContextWrapper构造函数中必须包含一个真正的Context引用,同时ContextWrapper中提供了attachBaseContext()用于给ContextWrapper对象指定真正的Context对象,调用ContextWrapper的方法都会被转向其包含的真正的Context对象。ContextThemeWrapper类,其内部包含了与主题Theme相关的接口,这里所说的主题就是指在AndroidManifest,xml中通过android:theme为Application元素或者Activity元素指定的主题。当然,只有Activity才需要主题,Service是不需要主题的,所以Service直接继承与ContextWrapper,Application同理。而ContextImpl类则真正实现了Context中的所有函数,应用程序中所调用的各种Context类的方法,其实现均来源于该类。**Context得两个子类分工明确,其中ContextImpl是Context的具体实现类,ContextWrapper是Context的包装类。** Activity、Application、Service虽都继承自ContextWrapper(Activity继承自ContextWrapper的子类ContextThemeWrapper),但它们初始化的过程中都会创建ContextImpl对象,由ContextImpl实现Context中的方法。
2623 |
2624 | **一个应用程序有几个Context?**
2625 |
2626 | 在应用程序中Context的具体实现子类就是:Activity、Service和Application。那么Context数量=Activity数量+Service数量+1。那么为什么四大组件中只有Activity和Service持有Context呢?BroadcastReceiver和ContextPrivider并不是Context的子类,它们所持有的Context都是其他地方传过去的,所以并不计入Context总数。
2627 |
2628 | **Context能干什么?**
2629 |
2630 | Context能实现的功能太多了,弹出Toast、启动Activity、启动Service、发送广播、启动数据库等等都要用到Context。
2631 |
2632 | ```java
2633 | TextView tv = new TextView(getContext());
2634 |
2635 | ListAdapter adapter = new SimpleCursorAdapter(getApplicationContext(), ...);
2636 |
2637 | AudioManager am = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);getApplicationContext().getSharedPreferences(name, mode);
2638 |
2639 | getApplicationContext().getContentResolver().query(uri, ...);
2640 |
2641 | getContext().getResources().getDisplayMetrics().widthPixels * 5 / 8;
2642 |
2643 | getContext().startActivity(intent);
2644 |
2645 | getContext().startService(intent);
2646 |
2647 | getContext().sendBroadcast(intent);
2648 | ```
2649 |
2650 | **Context的作用域**
2651 |
2652 | 虽然Context神通广大,但并不是随便拿到一个Context实例就可以为所欲为,它的使用还是有一些规则限制的。由于Context的具体实例是由ContextImpl类去实现的,因此在绝大多数场景下,Activity、Service和Application这三种类型的Context都是可以通用的。不过有几种场景比较特殊,比如启动Activity,还有弹出Dialog。出于安全原因的考虑,Android是不允许Activity或Dialog凭空出现的,一个Activity的启动必须要建立在另一个Activity的基础之上,也就是以此形成返回栈。而Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),因此在这种场景下,我们只能使用Activity类型的Context,否则将会报错。
2653 |
2654 | 
2655 |
2656 | 从上图我们可以发现Activity所持有的Context的作用域最广,无所不能,因此Activity继承至ContextThemeWrapper,而Application和Service继承至ContextWrapper,很显然ContextThemeWrapper在ContextWrapper的基础上又做了一些操作使得Activity变得更强大。着重讲一下不推荐使用的两种情况:
2657 |
2658 | 1. 如果我们用ApplicationContext去启动一个LaunchMode为standard的Activity的时候会报错:
2659 |
2660 | android.util.AndroidRuntimeException: Calling startActivity from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
2661 |
2662 | 这是因为非Activity类型的Context并没有所谓的任务栈,所以待启动的Activity就找不到栈了。解决这个问题的方法就是为待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,这样启动的时候就为它创建一个新的任务栈,而此时Activity是以singleTask模式启动的。所有这种用Application启动Activity的方式都不推荐,Service同Application。
2663 |
2664 | 2. 在Application和Service中去LayoutInflate也是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用,这种方式也不推荐使用。
2665 |
2666 | 一句话总结:**凡是跟UI相关的,都应该使用Activity作为Context来处理;其他的一些操作,Service、Activity、Application等实例都可以,当然了注意Context引用的持有,防止内存泄露。**
2667 |
2668 | **如何获取Context?**
2669 |
2670 | 有四种方法:
2671 |
2672 | 1. View.getContext 返回当前View对象的Context对象,通常是当前正在展示的Activity对象。
2673 | 2. Activity.getApplicationContext 获取当前Activity所在的进程的Context对象,通常我们使用Context对象时,要优先考虑这个全局的进程Context。
2674 | 3. ContextWrapper.getBaseContext() 用来获取一个ContextWrapper进行装饰之前的Context,可以使用这个方法,这个方法在实际开发中使用的不多,也不建议使用。
2675 | 4. Activity.this 返回当前Activity实例,如果是UI控件需要使用Activity作为Context对象,但是默认的Toast实际上使用ApplicationContext也可以。
2676 |
2677 | **getApplication()和getApplicationContext()的区别?**
2678 |
2679 | 其内存地址是一样的。Application本身就是一个Context,这里获取getApplicationContext得到的结果就是Application本身的实例。getApplication方法的语义性很强,就是用来获取Application实例的,但是这个方法只有在Activity和Service中才能调用的到。那么也许在绝大多数情况下我们都是在Activity或者Service中使用Application,但是如果在一些其他的场景,比如BroadcastReceiver中也想获取Application实例,这时就可以借助getApplicationContext方法了。
2680 |
2681 | ```java
2682 | public class MyReceiver extends BroadcastReceiver{
2683 | @Override
2684 | public void onReceive(Contextcontext,Intentintent){
2685 | Application myApp= (Application)context.getApplicationContext();
2686 | }
2687 | }
2688 | ```
2689 |
2690 |
2691 |
2692 | ### 4. AsyncTask详解
2693 |
2694 | #### Android中的线程
2695 |
2696 | 在操作系统中,线程是操作系统调度的最小单元,同时线程又是一种受限的系统资源,即线程不可能无限制的产生,并且**线程的创建和销毁都会有相应的开销**。当系统中存在大量的线程时,系统会通过时间片轮转的方式调度每个线程,因此线程不可能做到绝对的并行。
2697 |
2698 | 如果在一个进程中频繁的创建和销毁线程,显然不是高效地做法。正确的做法是采用线程池,一个线程池会缓存一定数量的线程,通过线程池就可以避免因为频繁创建和销毁线程所带来的系统开销。
2699 |
2700 | #### AsyncTask简介
2701 |
2702 | AsyncTask是一个抽象类,它是由Android封装的一个轻量级异步类,它可以在线程池中执行后台任务,然后把执行的进度和最终结果传递给主线程并在主线程中更新UI。
2703 |
2704 | AsyncTask的内部封装了两个线程池(SerialExecutor和THREAD_POOL_EXECUTOR)和一个Handle(InternalHandler)。
2705 |
2706 | 其中SerialExecutor线程池用于任务的排队,让需要执行的多个耗时任务,按顺序排列,THREAD_POLL_EXECUTOR线程池才真正的执行任务,InternalHandler用于从工作线程切换到主线程。
2707 |
2708 | ##### AsyncTask的泛型参数
2709 |
2710 | AsyncTask的类声明如下:
2711 |
2712 | ```java
2713 | public abstract class AsyncTask
2714 | ```
2715 |
2716 | AsyncTask是一个抽象泛型类。
2717 |
2718 | Params:开始异步任务时传入的参数类型
2719 |
2720 | Progress:异步任务执行过程中,返回下载进度值的类型
2721 |
2722 | Result:异步任务执行完成后,返回的结果类型
2723 |
2724 | 如果AsyncTask确定不需要传递具体参数,那么这三个泛型参数可以用Void来代替。
2725 |
2726 | ##### AsyncTask的核心方法
2727 |
2728 | **onPreExecute()**
2729 |
2730 | 这个方法会在**后台任务开始执行之前调用,在主线程执行**。用于进行一些界面上的初始化操作,比如显示一个进度条对话框等等。
2731 |
2732 | **doInBackground(Params...)**
2733 |
2734 | 这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。
2735 |
2736 | 任务一旦完成就可以通过return语句来将任务的执行结果进行返回,如果AsyncTask的第三个泛型参数指定的是Void,就可以不返回任务执行结果。**注意,这个方法中是不可以进行UI操作的,如果需要更新UI元素,比如说反馈当前任务的执行进度,可以调用publishProgress(Progress ...)方法来完成。**
2737 |
2738 | **onProgressUpdate(Progress...)**
2739 |
2740 | 当在后台任务中调用了publishProgress(Progress...)方法后,这个方法就很快被调用,方法中携带的参数就是在后台任务中传递过来的。**在这个方法中可以对UI进行操作,在主线程中进行,利用参数中的数值就可以对界面元素进行相应的更新。**
2741 |
2742 | **onPostExecute(Result)**
2743 |
2744 | 当doInBackground(Params...)执行完毕并通过return语句进行返回时,这个方法就很快被调用。返回的数据会作为参数传递到此方法中,**可以利用返回的数据来进行一些UI操作,在主线程中进行,比如说提醒任务执行的结果,以及关闭掉进度条对话框等等。**
2745 |
2746 | 上面几个方法的调用顺序为:onPreExecute() --> doInBackground() --> publishProgress() --> onProgressUpdate() --> onPostExecute()
2747 |
2748 | 如果不需要执行更新进度则为:onPreExecute() --> doInBackground() --> onPostExecute()
2749 |
2750 | 除了上面四个方法,AsyncTask还提供了onCancelled()方法,**它同样在主线程中执行,当异步任务取消时,onCancelled会被调用,这个时候onPostExecute()则不会调用,但是,AsyncTask的cancel()方法并不是真正的去取消任务,只是设置这个任务为取消状态,我们需要在doInBackground()判断终止任务。就好比想要终止一个线程,调用interrupt()方法,只是进行标记为中断,需要在线程内部进行标记判断然后中断线程。**
2751 |
2752 | ##### 使用AsyncTask的注意事项
2753 |
2754 | 1. 异步任务的实例必须在UI线程中创建,即AsyncTask对象必须在UI线程中创建。
2755 | 2. execute(Params ... params)方法必须在UI线程中调用。
2756 | 3. 不要手动调用onPreExecute()、doInBackground(Params ... params)、onProgressUpdate()、onPostExecute() 这几个方法。
2757 | 4. 不能在doInBackground()中更改UI组件信息。
2758 | 5. 一个任务实例只能执行一次,如果执行第二次将会抛异常。
2759 |
2760 |
2761 |
2762 | ### 5. Android虚拟机以及编译过程
2763 |
2764 | #### 什么是Dalvik虚拟机?
2765 |
2766 | Dalvik是Google公司自己设计用于Android平台的Java虚拟机,它是Android平台的重要组成部分,支持dex格式的Java应用程序的运行。dex格式是专门为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。Google对其进行了特定的优化,是的Dalvik具有**高效、简洁、节省资源**的特点。从Android系统架构图知,Dalvik虚拟机运行在Android的运行时库层。
2767 |
2768 | Dalvik作为面向Linux、为嵌入式操作系统设计的虚拟机,主要负责完成对象生命周期、堆栈管理、线程管理、安全和异常管理,以及垃圾回收等。另外,Dalvik早期并没有JIT编译器,知道Android2.2才加入了对JIT的技术支持。
2769 |
2770 | #### Dalvik虚拟机的特点
2771 |
2772 | 体积小,占用内存空间小。
2773 |
2774 | 专有的DEX可执行文件格式,体积更小,执行速度更快。
2775 |
2776 | 常量池采用32位索引值,寻址类方法名、字段名,常量更快。。
2777 |
2778 | 基于寄存器架构,并拥有一套完整的指令系统。
2779 |
2780 | 提供了对象生命周期管理,堆栈管理,线程管理,安全和异常管理以及垃圾回收等重要功能。
2781 |
2782 | 所有的Android程序都运行在Android系统进程里,每个进程对应着一个Dalvik虚拟机实例。
2783 |
2784 | #### Dalvik虚拟机与Java虚拟机的区别
2785 |
2786 | Dalvik虚拟机与传统的Java虚拟机有着许多不同点,两者并不兼容,它们显著的不同点主要表现在以下几个方面:
2787 |
2788 | **Java虚拟机运行的是Java字节码,Dalvik虚拟机运行的是Dalvik字节码。**
2789 |
2790 | 传统的Java程序经过编译,生成Java字节码保存在class文件中,Java虚拟机通过解码class文件中的内容来运行程序。而Dalvik虚拟机运行的是Dalvik字节码,所有的Dalvik字节码由Java字节码转换而来,并被打包到一个DEX可执行文件中。Dalvik虚拟机通过解码DEX文件来执行这些字节码。
2791 |
2792 | 
2793 |
2794 | **Dalvik可执行文件体积小,Android SDK中有一个叫dx的工具负责将Java字节码转换为Dalvik字节码。**
2795 |
2796 | 消除其中的冗余信息,重新组合形成一个常量池,所有的类文件共享同一个常量池。由于dx工具对常量池的压缩,使得相同的字符串常量在DEX文件中只出现一次,从而减小了文件的体积。
2797 |
2798 | 简单来讲,dex格式文件就是将多个class文件中公有的部分统一存放,去除冗余信息。
2799 |
2800 | **Java虚拟机与Dalvik虚拟机架构不同,这也是Dalvik与JVM之间最大的区别。**
2801 |
2802 | **Java虚拟机基于栈架构。**程序在运行时虚拟机需要频繁的从栈上读取或写入数据,这个过程需要更多的指令分配与内存访问次数,会耗费不少CPU时间,对于像手机设备资源有限来说,这是相当大的一笔开销。
2803 |
2804 | **Dalvik虚拟机基于寄存器架构。**
2805 |
2806 | 数据的访问通过寄存器间直接传递,这样的访问方式比基于栈方式要快很多。
2807 |
2808 | #### Dalvik虚拟机的结构
2809 |
2810 | 
2811 |
2812 | 一个应用首先经过DX工具将class文件转换成Dalvik虚拟机可以执行的dex文件,然后由类加载器加载原生类和Java类,接着由解释器根据指令集对Dalvik字节码进行解释、执行。最后,根据dvm_arch参数选择编译的目标机体系结构。
2813 |
2814 | #### Android APK编译打包流程
2815 |
2816 | 
2817 |
2818 | 1. AAPT(Android Asset Packaging Tools)工具会打包应用中的资源文件,如AndroidManifest.xml、layout布局中的xml等,并将xml文件编译成二进制形式,当然assets文件夹中的文件不会被编译,图片以及raw文件夹中的资源也会保持原有的形态,需要注意的是raw文件夹中的资源也会生成资源ID。AAPT编译完成后会生成R.java文件。
2819 | 2. AIDL工会将所有的aidl接口转换为java接口。
2820 | 3. 所有的Java源代码、R文件、接口都会编译器编译成.class文件。
2821 | 4. Dex工具会将上述产生的.class文件以及第三方库和其他class文件转化为dex(Dalvik虚拟机可执行文件)文件,dex文件最终会被打包进APK文件。
2822 | 5. apkbuilder会把编译后的资源和其他资源文件同dex文件一起打入APK中。
2823 | 6. 生成APK文件之后,,需要对其签名才能安装到设备上,平时测试都会使用debug keystore,当发布应用时必须使用release版的keystore对应用进行签名。
2824 | 7. 如果对APK正式签名,还需要使用zipalign工具对APK进行对齐操作,这样做的好处是当应用运行时能提高速度,但是会相应的增加内存开销。
2825 |
2826 | **总结:编译 --> DEX --> 打包 --> 签名和对齐**
2827 |
2828 | #### ART虚拟机与Dalvik虚拟机的区别
2829 |
2830 | ##### 什么是ART?
2831 |
2832 | ART代表Android Runtime,其处理应用程序执行的方式完全不同于Dalvik,Dalvik是依靠一个Just-In-Time(JIT)编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运行。ART则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,这一机制叫Ahead-Of-Time(AOT)编译。在移除解释代码这一过程后,应用程序执行将更加效率。启动更快。
2833 |
2834 | ##### ART优点:
2835 |
2836 | 1. 系统性能的显著提升。
2837 | 2. 应用启动更快、运行更快、体验更流畅、触摸反馈更及时。
2838 | 3. 更长的电池续航能力
2839 | 4. 支持更低的硬件。
2840 |
2841 | ##### ART缺点:
2842 |
2843 | 1. 更大的存储空间占用,可能会增加10%-20%
2844 | 2. 更长的应用安装时间
2845 |
2846 | #### ART虚拟机相对于Dalvik虚拟机的提升
2847 |
2848 | ##### 预编译
2849 |
2850 | 在Dalvik中,如同其他大多数JVM一样,都采用的是JIT来做及时翻译(动态翻译),将dex或odex中并排的Dalvik code(或者叫smali指令集)运行态翻译成native code去执行。JIT的引入使得Dalvik提升了3-6倍的性能。
2851 |
2852 | 而在ART中,完全抛弃了Dalvik的JIT,使用了AOT直接在安装时将其完全翻译成native code。这一技术的引入,使得虚拟机执行指令的速度又一重大提升。
2853 |
2854 | ##### 垃圾回收机制
2855 |
2856 | 首先介绍下Dalvik的GC过程,主要有四个过程:
2857 |
2858 | 1. 当GC被触发时候,其会去查找所有活动的对象,这个时候整个程序与虚拟机内部的所有线程就会挂起,这样目的是在较少的堆栈里找到所引用的对象。
2859 | 2. GC对符合条件的对象进行标记。
2860 | 3. GC对标记的对象进行回收。
2861 | 4. 恢复所有线程的执行现场继续运行。
2862 |
2863 | **Dalvik这么做的好处是,当pause了之后,GC势必是相当快速的,但是如果出现GC频繁并且内存吃紧势必会导致UI卡顿、掉帧、操作不流畅等等。**
2864 |
2865 | 后来ART改善了这种GC方式,主要的改善点在将其**非并发过程改成了部分并发,还有就是对内存的重新分配管理。**
2866 |
2867 | 当ART GC发生时:
2868 |
2869 | 1. GC将会锁住Java堆,扫描并进行标记。
2870 | 2. 标记完毕释放掉Java堆的锁,并且挂起所有线程。
2871 | 3. GC对标记的对象进行回收。
2872 | 4. 恢复所有线程的执行继续运行。
2873 | 5. **重复2-4直到结束。**
2874 |
2875 | 可以看出整个过程做到了部分并发使得时间缩短,GC效率提高两倍。
2876 |
2877 | ##### 提高内存使用,减少碎片化
2878 |
2879 | Dalvik内存管理特点是:内存碎片化严重,当然这也是标记清除算法带来的弊端。
2880 |
2881 | **ART的解决:** 在ART中,它将Java分了一块空间命名为 Large-Object-Space,这个内存空间的引入用来专文存放大对象,同时ART又引入了 moving collector 的技术,即将不连续的物理内存快进行对齐。对齐之后内存碎片化就得到了很好的解决。Large-Object-Space的引入是因为moving collector对大块内存的位移时间成本太高。据官方统计,ART的内存利用率提高了10倍左右,大大提高了内存的利用率。
2882 |
2883 |
2884 |
2885 | ### 6. 进程保活方案
2886 |
2887 | #### 保活的两个方案
2888 |
2889 | * 提高进程优先级,降低进程被杀死的概率
2890 | * 在进程被杀死后,进行拉活
2891 |
2892 | ##### 进程的优先级,划分五级:
2893 |
2894 | 1. 前台进程(Foreground process)
2895 | 2. 可见进程(Visible process)
2896 | 3. 服务进程(Service process)
2897 | 4. 后台进程(Background process)
2898 | 5. 空进程(Empty process)
2899 |
2900 | **前台进程一般有以下特点:**
2901 |
2902 | * 拥有用户正在交互的Activity(已调用onResume)
2903 | * 拥有某个Service,后者绑定到用户正在交互的Activity
2904 | * 拥有正在前台运行的Service(服务已调用startForeground)
2905 | * 拥有一个正执行生命周期回调的Service(onCreate,onStart或onDestory)
2906 | * 拥有其正执行其onReceive()方法的BroadcastReceiver
2907 |
2908 | #### 提高进程优先级
2909 |
2910 | * 利用Activity提升权限
2911 |
2912 | 监控手机锁屏解锁事件,在屏幕锁屏时启动1像素的Activity,在用户解锁时将Activity销毁,注意该Activity需设计成用户无感知。
2913 |
2914 | * Notification提升权限
2915 |
2916 | Android中Service的优先级为4,通过setForeground接口可以将后台Service设置为前台Service,使进程的优先级由4提升为2,从而是进程的优先级仅仅低于用户当前正在交互的进程,与可见进程优先级一致,使进程被杀死的概率大大降低。
2917 |
2918 | #### 进程死后拉活
2919 |
2920 | * 利用系统广播拉活
2921 |
2922 | 在发生特定系统事件时,系统会发出相应的广播,通过在AndroidManifest中静态注册对应的广播监听器,即可在发生响应事件时拉活。
2923 |
2924 | * 利用第三方应用广播拉活
2925 |
2926 | 该方案总的设计思想与接收系统广播类似,不同的是该方案为接收第三方Top应用广播。通过反编译第三方Top应用,如微信、支付宝等等,找出它们外发的广播,在应用中进行监听,这样当这些应用发出广播时,就会将我们的应用拉活。
2927 |
2928 | * 利用系统Service机制拉活
2929 |
2930 | 将Service设置为START_STICKY,利用系统机制在Service挂掉后自动拉活。
2931 |
2932 | * 利用Native进程拉活
2933 |
2934 | 利用Linux中的fork机制创建Native进程,在Native进程中监控主进程的存活,当主进程挂掉后,在Native进程中立即对主进程进行拉活。
2935 |
2936 | 原理:在Android中所有进程和系统组件的生命周期受ActivityManagerService的统一管理。而且,通过Linux的fork机制创建的进程为纯Linux进程,其生命周期不受Android管理。
2937 |
2938 | * 利用 JobScheduler机制拉活
2939 |
2940 | 在Android5.0以后系统对Native进程等加强了管理,Native拉活方式失效。系统在Android5.0以后版本提供了 JobScheduler接口,系统会定时调用该进程以使应用进行一些逻辑操作。
2941 |
2942 | * 利用账号同步机制拉活
2943 |
2944 | Android系统的账号同步机制会定期同步账号进行,该方案目的在于利用同步机制进行进程的拉活。
2945 |
2946 | #### 其他有效的拉活方案
2947 |
2948 | 相互唤醒。
2949 |
2950 | **参考:**
2951 |
2952 | [进程保活方案1](https://www.jianshu.com/p/845373586ac1)
2953 |
2954 | [Android进程保活招式大全](http://geek.csdn.net/news/detail/95035)
2955 |
2956 |
2957 |
2958 | ### 7. Android 消息机制
2959 |
2960 | #### 消息机制简介
2961 |
2962 | 在Android中使用消息机制,我们首先想到的就是Handler。没错,Handler是Android消息机制的上层接口。我们通常只会接触到Handler和Message开完成消息机制,其实内部还有两大助手共同完成消息传递。
2963 |
2964 | #### 消息机制模型
2965 |
2966 | 消息机制主要包含:MessageQueue、Handler、Looper和Message这四大部分。
2967 |
2968 | * Message
2969 |
2970 | 需要传递的消息,可以传递数据
2971 |
2972 | * MessageQueue
2973 |
2974 | 消息队列,但是它的内部实现并不是用的队列,实际上是通过一个单链表的数据结构来维护消息列表,因为单链表在插入和删除上比较有优势。主要功能是向消息池传递消息(MessageQueue.enqueueMessage)和取走消息池的消息(MessageQueue.next)
2975 |
2976 | * Handle
2977 |
2978 | 消息辅助类,主要功能是向消息池发送各种消息事件(Handler.sendMessage)和处理相应消息事件(Handler.handleMessage)
2979 |
2980 | * Looper
2981 |
2982 | 不断循环执行(Looper.loop),从MessageQueue中读取消息,按分发机制将消息分发给目标处理者。
2983 |
2984 | #### 消息机制的架构
2985 |
2986 | **运行流程:**
2987 |
2988 | 在子线程执行完耗时操作,当Handler发送消息时,将会调用MessageQueue.enqueueMessage,向消息队列中添加消息。当通过Looper.loop开启循环后,会不断的从线程池中读取消息,即调用MessageQueue.next,然后调用目标Handler(即发送该消息的Handler)的dispatchMessage方法传递消息,然后返回到Handler所在线程,目标Handler收到消息,调用handleMessage方法,接收消息,处理消息。
2989 |
2990 | **MessageQueue、Handler和Looper三者之间的关系:**
2991 |
2992 | 每个线程中只能存在一个Looper,Looper是保存在ThreadLocal中。主线程已经创建一个Looper,所以在主线程中不需要在创建Looper,但是在其他线程中需要创建Looper。每个线程中可以有多个Handler,即一个Looper可以处理来自多个Handler的消息。Looper中维护一个MessageQueue,来维护消息队列,消息队列中的Message可以来自不同的Handler。
2993 |
2994 | #### 总结
2995 |
2996 | 
2997 |
2998 | Android消息机制之ThreadLocal的工作原理:
2999 |
3000 | Looper中还有一个特殊的概念,那就是ThreadLocal,ThreadLocal并不是线程,它的作用是可以在每个线程中存储数据。大家知道,Handle创建的时候会采用当前线程的Looper来构造消息循环系统,那么Handle内部如何获取当前线程的Looper呢?这就要使用ThreadLocal了,ThreadLocal可以在不同的线程之中互不干扰的存储并提供数据,通过ThreadLocal可以轻松的获取每个线程的Looper。当然,需要注意的是,线程默认是没有Looper的,如果需要使用Handler就必须为线程创建Looper。大家经常提到的主线程,也叫UI线程,它就是ActivityThread,ActivityThread被创建时就会初始化Looper,这也是在主线程默认可以使用Handle的原因。
3001 |
3002 | ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其他线程来说无法获取到数据。在日常开发中用到ThreadLocal的地方很少,但是在某些特殊的场景下,通过ThreadLocal可以轻松的实现一些看起来很复杂的功能,这一点在Android源码中也有所体现,比如Looper、ActivityThread以及AMS中都用到了ThreadLocal。具体到ThreadLocal的使用场景,这个不好统一来描述,一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以采用ThreadLocal。比如对于Handle来说,它需要获取当前线程的Looper,很显然Looper的作用域就是线程并且不同线程具有不同的Looper,这个时候通过ThreadLocal就可以轻松的实现Looper在线程中的存取。
3003 |
3004 | ThreadLocal另外一个使用场景是复杂逻辑下的对象传递,比如监听器的传递,有些时候一个线程中的任务过于复杂,这可能表现为函数调用栈比较深以及代码入口的多样性,在这种情况下,我们又需要监听器能够贯穿整个线程的执行过程,这个时候可以怎么做呢?其实就可以采用ThreadLocal,采用ThreadLocal可以让监听器作为线程内的局部对象而存在,在线程内部只要通过get方法就可以获取到监听器。而如果不采用ThreadLocal,那么我们能想到的可能就是一下两种方法:
3005 |
3006 | 1. 将监听器通过参数的形式在函数调用栈中进行传递
3007 |
3008 | 在函数调用栈很深的时候,通过函数参数来传递监听器对象几乎是不可接受的
3009 |
3010 | 2. 将监听器作为静态变量供线程访问
3011 |
3012 | 这是可以接受的,但是这种状态是不具有可扩充性的,比如如果同时有两个线程在执行,那就需要提供两个静态的监听器对象,如果有十个线程在并发执行呢?提供十个静态的监听器对象?这显然是不可思议的。而采用ThreadLocal每个监听器对象都在自己的线程内部存储,根本就不会有这个问题。
3013 |
3014 | ```
3015 | ThreadLocal threadLocal = new ThreadLocal<>();
3016 | threadLocal.set(true);
3017 | Log.i(TAG, "getMsg: MainThread"+threadLocal.get());
3018 | new Thread("Thread#1"){
3019 | @Override
3020 | public void run() {
3021 | threadLocal.set(false);
3022 | Log.i(TAG, "run: Thread#1" + threadLocal.get());
3023 | }
3024 | }.start();
3025 | new Thread("Thread#2"){
3026 | @Override
3027 | public void run() {
3028 | Log.i(TAG, "run: Thread#2" + threadLocal.get());
3029 | }
3030 | }.start();
3031 |
3032 | 输出:true、false、null
3033 | ```
3034 |
3035 | 虽然在不同线程中访问的是同一个ThreadLocal对象,但是它们获取到的值却是不一样的。不同线程访问同一个ThreadLocal的get方法,ThreadLocal内部会从各自的线程中取出一个数组,然后再从数据中根据当前ThreadLocal的索引去查找出对应的value值,很显然,不同线程中的数组是不同的,这就是为什么通过ThreadLocal可以在不同的线程中维护一套数据副本并且彼此互不干扰。
3036 |
3037 | ThreadLocal是一个泛型类,里面有两个重要方法:get()和set()方法。它们所操作的对象都是当前线程的localValues对象的table数组,因此在不同线程中访问同一个ThreadLocal的get和set方法,它们对ThreadLocal所做的读写操作仅限于各自线程的内部,这就是为什么ThreadLocal可以在多个线程中互不干扰的存储和修改数据。
3038 |
3039 | [https://blog.csdn.net/singwhatiwanna/article/details/48350919](https://blog.csdn.net/singwhatiwanna/article/details/48350919)
3040 |
3041 | ### 8. Window、Activity、DecorView以及ViewRoot之间的关系
3042 |
3043 | #### 职能简介
3044 |
3045 | **Activity**
3046 |
3047 | Activity并不负者视图控制,它只是控制生命周期和处理事件。真正控制视图的是Window。一个Activity包含了一个Window,Window才是真正代表一个窗口。**Activity就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来与Window以及View进行交互。**
3048 |
3049 | **Window**
3050 |
3051 | Window是视图的承载器,内部持有一个DecorView,而这个DecorView才是view的跟布局。Window是一个抽象类,实际在Activity中持有的是其子类PhoneWindow。PhoneWindow中有个内部类DecorView,通过创建DecorView来加载Activity中设置的布局。Window通过WindowManager将DecorView加载其中,并将DecorView交给ViewRoot,进行视图绘制以及其他交互。
3052 |
3053 | **DecorView**
3054 |
3055 | DecorView是FrameLayout的子类,它可以被认为是Android视图树的根节点视图。
3056 |
3057 | DecorView作为顶级View,一般情况下它内部包含一个竖直方向的LinearLayout,**在这个LinearLayout里面有上下三个部分,上面是个ViewStub,延迟加载的视图(应该是设置ActionBar,根据Theme设置),中间的是标题栏(根据Theme设置,有的布局没有),下面是内容栏。**在Activity中通过setContentView所设置的布局文件其实就是被加到内容栏之中的,成为其唯一子View。
3058 |
3059 | **ViewRoot**
3060 |
3061 | ViewRoot可能比较陌生,但是其作用非常重大。所有View的绘制以及事件分发等交互都是通过它来执行或传递的。
3062 |
3063 | ViewRoot对应ViewRootImpl类,它是连接WindowManagerService和DecorView的纽带,View的三大流程(测量、布局、绘制)均通过ViewRoot来完成。
3064 |
3065 | ViewRoot并不属于View树的一份子。从源码实现上来看,它既是非View的子类,也是非View的父类,但是,它实现了ViewParent接口,这让它可以作为View的名义上的父视图。RootView继承了Handler类,可以接收事件并分发,Android的所有触屏事件,按键事件、界面刷新等事件都是通过ViewRoot来进行分发的。
3066 |
3067 | 
3068 |
3069 |
3070 |
3071 | #### 总结
3072 |
3073 | Activity就像个控制器,不负责视图部分。Window像个承载器,装着内部视图。DecorView就是个顶级视图,是所有View的最外层布局。ViewRoot像个连接器,负者沟通,通过硬件感知来通知视图,进行用户之间的交互。
3074 |
3075 | ### 9. Android事件分发机制
3076 |
3077 | [图解 Android 事件分发机制](https://www.jianshu.com/p/e99b5e8bd67b)
3078 |
3079 | 简述:
3080 |
3081 | * Activity --> ViewGroup --> View 责任链模式
3082 |
3083 |
3084 | * dispatchTouchEvent 和 onTouchEvent 一旦 return true,事件就停止传递了(到达终点,没有谁再能收到这个事件),对于 return true 我们经常说事件被消费了,消费了的意思就是事件走到这里就是终点,不会往下传,没有谁能在收到这个事件了。
3085 | * dispatchTouchEvent 和 onTouchEvent return false 的时候事件都会回传给父控件的 onTouchEvent处理。对于dispatchTouchEvent返回false的含义应该是:事件停止往子View传递和分发同时开始往父控件回溯(父控件的onTouchEvent开始从下往上回传直到某个onTouchEvent return true),事件分发机制就像递归,return false 的意义就是递归停止然后开始回溯。
3086 | * 对于onTouchEvent return false就比较简单了,它就是不消费事件,并让事件继续往父控件的方向从下往上流动。
3087 | * oninterceptTouchEvent,用于事件拦截,只存在于ViewGroup中,如果返回true就会交给自己的onTouchEvent处理,如果不拦截就是往子控件往下传递。默认是不会去拦截的,因为子View也需要这个事件,所以onInterceptTouchEvent拦截器return super和false是一样的,事件往子View的dispatchTouchEvent传递。
3088 | * 对于ViewGroup,dispatchTouchEvent,之前说的return true就是终结传递,return false就是回溯到父View的onTouchEvent。那么ViewGroup怎样通过dispatchTouchEvent方法能把事件分发到自己的onTouchEvent处理呢?return false 和 true 都不行,那么只能通过 onInterceptTouchEvent把事件拦截下来给自己的onTouchEvent,所以ViewGroup的dispatchTouchEvent方法的super默认实现就是去调用onInterceptTouchEvent,记住这一点。
3089 |
3090 | **总结:**
3091 |
3092 | * 对于dispatchTouchEvent,onTouchEvent,return true 是终结事件传递,return false是回溯父View的onTouchEvent方法
3093 | * ViewGroup想把事件分发给自己的onTouchEvent处理,需要拦截器onInterceptTouchEvent方法return true把事件拦截下来
3094 | * ViewGroup的拦截器onInterceptTouchEvent默认是不拦截的,所以return super和return false是一样的
3095 | * View没有拦截器,为了让View可以把事件分发给自己的onTouchEvent处理,View的dispatchTouchEvent默认实现(super)就是把事件分发给自己的onTouchEvent
3096 |
3097 | ### 10. dp、sp、px的理解以及相互转换
3098 |
3099 | **px**
3100 |
3101 | 像素,对应于屏幕上的实际像素。在画分割线的可以用到,用其他单位画可能很模糊。
3102 |
3103 | **dp、dip**
3104 |
3105 | 与密度无关的像素单位,基于屏幕的物理尺寸的抽象单位,在160dpi的屏幕上1dp相当于1px,一般用于空间大小的单位。
3106 |
3107 | **sp**
3108 |
3109 | 与缩放无关的像素单位,类似dp,不用之处在于它还会根据用户字体大小配置而缩放。开发中指定字体大小时建议使用sp,因为它会根据屏幕密度和用户字体配置而适配
3110 |
3111 | ```java
3112 | DisplayMetrics dm = getResources().getDisplayMetrics();
3113 | int dpi = dm.densityDpi; // 屏幕Dpi
3114 | int width = dm.widthPixels; // 当前屏幕可用空间的宽(像素)
3115 | int height = dm.heightPixels; // 高(像素)
3116 | float density = dm.density; // 屏幕密度
3117 | float scale = dm.scaledDensity; // 字体缩放比例
3118 |
3119 | // 获得设备的配置信息
3120 | Configuration c = getResources().getConfiguration();
3121 | int widthDp = c.screenWidthDp; // 当前屏幕可用空间的宽(dp)
3122 | int heightDp = c.screenHeightDp; // 高(dp)
3123 | int dpi2 = c.densityDpi; // 屏幕Dpi
3124 | float fontScale = c.fontScale; // 字体缩放比例
3125 | ```
3126 |
3127 | ```java
3128 | /**
3129 | * 将px值转换为dip或dp值
3130 | * @param pxValue
3131 | * @return
3132 | */
3133 | public static int px2dip(Context context, float pxValue) {
3134 | final float scale = context.getResources().getDisplayMetrics().density;
3135 | return (int) (pxValue / scale + 0.5f);
3136 | }
3137 |
3138 | /**
3139 | * 将dip或dp值转换为px值
3140 | *
3141 | * @param dipValue
3142 | * @return
3143 | */
3144 | public static int dip2px(Context context, float dipValue) {
3145 | final float scale = context.getResources().getDisplayMetrics().density;
3146 | return (int) (dipValue * scale + 0.5f);
3147 | }
3148 |
3149 | /**
3150 | * 将px值转换为sp值
3151 | * @param pxValue
3152 | * @return
3153 | */
3154 | public static int px2sp(Context context, float pxValue) {
3155 | final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
3156 | return (int) (pxValue / fontScale + 0.5f);
3157 | }
3158 |
3159 | /**
3160 | * 将sp值转换为px值
3161 | * @param spValue
3162 | * @return
3163 | */
3164 | public static int sp2px(Context context, float spValue) {
3165 | final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
3166 | return (int) (spValue * fontScale + 0.5f);
3167 | }
3168 | ```
3169 |
3170 | ### 11. RelativeLayout和LinearLayout在实现效果同等的情况下使用哪个?为什么?
3171 |
3172 | [Android中RelativeLayout和LinearLayout性能分析](https://www.jianshu.com/p/8a7d059da746)
3173 |
3174 | 选择LinearLayout,因为RelativeLayout在measure过程需要两次。
3175 |
3176 | ```java
3177 | //RelativeLayout源码
3178 | View[] views = mSortedHorizontalChildren;
3179 | int count = views.length;
3180 | for (int i = 0; i < count; i++) {
3181 | /**/
3182 | measureChildHorizontal(child, params, myWidth, myHeight);
3183 | }
3184 | /**/
3185 | for (int i = 0; i < count; i++){
3186 | /***/
3187 | measureChild(child, params, myWidth, myHeight);
3188 | }
3189 |
3190 | //LinearLayout
3191 | @Override
3192 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3193 | if (mOrientation == VERTICAL) {
3194 | measureVertical(widthMeasureSpec, heightMeasureSpec);
3195 | } else {
3196 | measureHorizontal(widthMeasureSpec, heightMeasureSpec);
3197 | }
3198 | }
3199 |
3200 | ```
3201 |
3202 | 从源码我们发现RelativeLayout会对子View做两次measure。这是为什么呢?首先RelativeLayout中子View的排列方式是基于彼此的依赖关系,而这个依赖关系可能和布局中View的顺序并不相同,在确定每个子View的位置的时候,就需要先给所有的子View排序一下。又因为RelativeLayout允许A B两个子View,横向上B依赖于A,纵向上A依赖于B,所以需要横向纵向分别进行一次排序测量。
3203 |
3204 | RelativeLayout另外一个性能问题:
3205 |
3206 | View的measure方法里对绘制过程做了一个优化,如果我们的子View没有要求强制刷新,而父View给子View的传入值也没有变化,也就说子View的位置没有变化,就不会做无谓的measure。但是上面已经说了RelativeLayout要做两次measure,而在做横向测量时,纵向的测量结果尚未完成,只好暂时使用myHeight传入子View系统,假如子View的Height不等于(设置了margin)myHeight的高度,那么measure中优化则不起作用,这一过程将进一步影响RelativeLayout的绘制性能。而LinearLayout则无这方面的担忧,解决这个问题也很好办,如果可以,尽量使用padding代替margin。
3207 |
3208 | **结论**
3209 |
3210 | * RelativeLayout会让子View调用两次onMeasure,LinearLayout再有weight时,也会调用子View两次onMeasure
3211 | * RelativeLayout的子View如果高度和RelativeLayout不同,则会引发效率问题。当子View很复杂时,这个问题会更加严重。如果可以,尽量使用padding代替margin
3212 | * 在不影响层级深度的情况下,使用LinaerLayout和FrameLayout而不是RelativeLayout
3213 |
3214 | ### 12. 布局相关的 \、\ 控件作用及实现原理
3215 |
3216 | [从源码角度分析ViewStub 疑问与原理](https://blog.csdn.net/androiddevelop/article/details/46632323)
3217 |
3218 | * ViewStub本身是一个视图,会被添加到界面上,之所以看不到是因为其设置了隐藏与不绘制
3219 | * 当调用infalte或者ViewStub.setVisibilty(View.VISIBLE)时(两个都使用infalte逻辑),先从父视图上把当前ViewStub删除,再把加载的android:layout视图添加上
3220 | * 把ViewStub LayoutParams 添加到加载的android:layout视图上,而其根节点的LayoutParams设置无效
3221 | * ViewStub是指用来占位的视图,通过删除自己并添加android:layout视图达到懒加载效果
3222 |
3223 |
3224 |
3225 |
3226 | ### 13. Fragment详解
3227 |
3228 | #### 生命周期
3229 |
3230 | 
3231 |
3232 | #### Fragment使用方式
3233 |
3234 | **静态使用**
3235 |
3236 | 步骤:
3237 |
3238 | 1. 创建一个类继承Fragment,重写onCreateView方法,来确定Fragment要显示的布局
3239 | 2. 在Activity的布局xml文件中添加< fragment >标签,并声明name属性为Fragemnt的全限定名,**而且必须给该控件(fragment)一个id或者tag**
3240 |
3241 | **动态使用**
3242 |
3243 | 步骤:
3244 |
3245 | 1. 在Activity的布局xml文件中添加一个FrameLayout,此控件是Fragment的容器。
3246 | 2. 准备好Fragment并实例化,获取Fragment管理器对象 FragmentManager。
3247 | 3. 然后通过Fragment管理器调用beginTransaction(),实例化FragmentTransaction对象,即开启事务。
3248 | 4. 执行事务并提交。FragmentTransaction对象有以下几种方法:
3249 | * add() 向Activity中添加一个Fragment
3250 | * remove() 从Activity中移除一个Fragment,如果被移除的Fragment没有添加到回退栈,这个Fragment实例将会被销毁
3251 | * replace() 使用一个Fragment替换当前的,实际上就是remove + add
3252 | * hide() 隐藏当前的Fragment,仅仅设为不可见,并不会销毁
3253 | * show() 显示之前隐藏的Fragment
3254 | * detach() 会将view从UI中移除,和remove()不同,此时fragment的状态依然由FragmentManager维护
3255 | * attach() 重建View视图,附加到UI上并显示
3256 | * commit() 提交事务
3257 |
3258 | **回退栈**
3259 |
3260 | Fragment的回退栈是用来保存每一次Fragment事务发生的变化,如果将Fragment任务添加到回退栈,当用户点击后退按钮时,将会看到上一次保存的Fragment,一旦Fragment完全从回退栈中弹出,用户再次点击后退键,则会退出当前Activity。
3261 |
3262 | FragmentTransaction.addToBackStack(String) 把当前事务的变化情况添加到回退栈。
3263 |
3264 | **Fragment与Activity之间的通信**
3265 |
3266 | Fragment依附于Activity存在,因此与Activity之间的通信可以归纳为以下几种:
3267 |
3268 | * 如果Activity中包含自己管理的Fragment的引用,可以通过访问其public方法进行通信
3269 | * 如果Activiy中没有保存任何Fragment的引用,那么没关系,每个Fragment都有一个唯一的TAG或者ID,可以通过getFragmentManager.findFragmentByTag()或者findFragmentById()获得Fragment实例,然后进行操作
3270 | * Fragment中可以通过getActivity()得到当前绑定的Activity实例,然后进行操作
3271 | * setArguments(Bundle) / getArguments()
3272 |
3273 | **通信优化**
3274 |
3275 | 接口实现
3276 |
3277 | **如何处理运行时配置发生变化**
3278 |
3279 | 横竖屏切换时导致Fragment多次重建绘制:
3280 |
3281 | 不要在构造函数中传递参数,最好通过setArguments()传递。
3282 |
3283 | 在onCreate(Bundle savedInstanceState)中判断savedInstanceState为空时在重建,当发生重建时,原本的数据如何保持?类似Activity,Fragment也有onSaveInstanceState方法,在此方法中进行保存数据,然后在onCreate或者onCreateView或者onActivityCreated进行恢复都可以。
3284 |
3285 |
3286 |
3287 | ### 14. Json、XML
3288 |
3289 | Json是一种轻量级的数据交换格式,具有良好的可读和便于编写的特性。XML即扩展标记语言,用来标记电子文件使其具有结构性的标记语言,可以用来标记数据。定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。
3290 |
3291 | JSON在编码和解码上优于XML,并且数据体积更下,解析更快,与JavaScript交互更加方便,但是对数据的描述性较XML差。
3292 |
3293 |
3294 |
3295 | ### 15. Assets目录与res目录的区别
3296 |
3297 | assets目录与res下的raw、drawable目录一样,也可用来存放资源文件,但它们三者区别如下:
3298 |
3299 | | | assets | res/raw | res/drawable |
3300 | | ----------- | -------- | --------- | -------------- |
3301 | | 获取资源方式 | 文件路径+文件名 | R.raw.xxx | R.drawable.xxx |
3302 | | 是否被压缩 | false | false | true(失真压缩) |
3303 | | 能否获取子目录下的资源 | true | false | false |
3304 |
3305 | res/raw和assets的区别:
3306 |
3307 | res/raw中的文件会被映射到R.java文件中,访问的时候直接使用资源ID即可,assets文件夹下的文件不会被映射到R文件中,访问的时候需要AssetManager类。
3308 |
3309 | res/raw不可以有目录结构,而assets则可以有目录结构,也就是assets目录下可以再建立文件夹。
3310 |
3311 | 读取res/raw下的文件资源,通过以下方式获取输入流来进行写操作:
3312 |
3313 | ```java
3314 | InputStream is = getResources().openRawResource(R.id.filename);
3315 | ```
3316 |
3317 | 读取assets下的文件资源,通过以下方式获取输入流进行写操作,使用AssetManager。
3318 |
3319 | 注意:
3320 |
3321 | 1. AssertManager中不能处理单个超过1M的文件,而raw没有这个限制
3322 | 2. assets文件夹是存放不进行编译加工的原生文件,即该文件夹里面的文件不会像xml、java文件被预编译,可以存放一些图片、html、js等等
3323 |
3324 |
3325 |
3326 | ### 16. View视图绘制过程原理
3327 |
3328 | View视图绘制需要搞清楚两个问题,一个是从哪里开始绘制,一个是怎么绘制?
3329 |
3330 | 从哪里开始绘制?我们平常使用Activity的时候,都会调用setContentView来设置布局文件,没错,视图绘制就是从这个方法开始。
3331 |
3332 | 怎么绘制?
3333 |
3334 | 在我们的Activity中调用了setContentView之后,会转而执行PhoneWindow的setContentView,在这个方法里面会判断我们存放内容的ViewGroup(这个ViewGroup可以是DecorView也可以是DecorView的子View)是否存在。不存在的话,则会创建一个DecorView处理,并且会创建出相应的窗体风格,存在的话则会删除原先的ViewGroup上面已有的View,接着会调用LayoutInflater的inflate方法以pull解析的方式将当前布局文件中存在的View通过addView的方式添加到ViewGroup上面来,接着在addView方法里面就会执行我们常见的invalidate方法了,这个方法不只是在View视图绘制的过程中经常用到,其实动画的实现原理也是不断的调用这个方法来实现视图不断重绘的,执行这个方法的时候会调用父View的invalidateChild方法,这个方法是属于ViewParent的,ViewGroup以及ViewRootImpl中都会他进行了实现,invalidateChild里面主要做的是就是通过do while循环一层一层计算出当前View的四个点所对应的矩阵在ViewRoot中所对应的位置,那么有了这个矩阵的位置之后最终都会执行ViewRootImpl的invalidateChildInParent方法,执行这个方法的时候首先会检查当前线程是不是主线程,因为我们要开始准备更新UI了,不是主线程的话是不允许更新UI的,接着就会执行scheduleTraversals方法了,这个方法会通过handler来执行doTraversal方法,在这个方法里面就见到了我们平常所熟悉的View视图绘制的起点方法performTraversals了。
3335 |
3336 | 那么接下来就是真正的视图绘制流程了,大体上讲View的绘制流程经历了Measure测量、Layout布局以及Draw绘制的三个过程,具体来讲是从ViewRootImpl的performTraversals方法开始,首先执行的将是performMeasure方法,这个方法里面会传入两个MeasureSpec类型的参数,它在很大程度上决定了View的尺寸规格,对于DecorView来说宽高的MeasureSpec值的获取与窗口尺寸以及自身的LayoutParams有关,对于普通View来说其宽高的MeasureSpec值获取由父容器以及自身的LayoutParams属性共同决定,在performMeasure里面会执行measure方法,在measure方法里面会执行onMeasure方法,到这里Measure测量过程对View与ViewGroup来说是没有区别的,但是从onMeasure开始两者有差别了,因为View本身已经不存在子View了,所以他onMeasure方法将执行setMeasuredDimension方法,该方法会设置View的测量值,但是对于ViewGroup来说,因为它里面还存在着子View,那么我们就需要继续测量它里面的子View了,调用的方法是measureChild方法,该方法内部又会执行measure方法,而measure方法转而又会执行onMeasure方法,这样不断的递归进行下去,直到整个View树测量结束,这样performMeasure方法执行结束了。接着便是执行performLayout方法了,performMeasure只是测量出了View树中View的大小了,但是还不知道View的位置,所以也就出现了performLayout方法了,performLayout方法首先会执行layout方法,以确定View自身的位置,如果当前View是ViewGroup的话,则会执行onLayout方法。在onLayout方法里面又会递归的执行layout方法,直到当前遍历到的View不再是ViewGroup为止,这样整个layout布局过程就结束了。在View树中View的大小以及位置都确定之后,接下来就是真正的绘制View显示在界面的过程了,该过程首先从performDraw方法开始,performDraw首先会执行draw方法,在draw方法中首先绘制背景,接着调用onDraw方法绘制自己,如果当前View是ViewGroup的话,还要调用dispatchDraw方法绘制当前ViewGroup的子View,而dispatchDraw方法里面实际上是通过drawChild方法间接调用draw方法形成递归绘制整个View树,直到当前View不再是ViewGroup为止,这样整个View的绘制过程就结束了。
3337 |
3338 | 总结:
3339 |
3340 | * ViewRootImpl会调用performTraversals(),其内部会调用performMeasure()、performLayout、performDraw
3341 | * performMeasure会调用最外层的ViewGroup的measure() --> onMeasure() ,ViewGroup的onMeasure()是抽象方法,但其提供了measureChildren(),这之中会遍历子View然后循环调用measureChild(),传入MeasureSpec参数,然后调用子View的measure()到View的onMeasure() -->setMeasureDimension(getDefaultSize(),getDefaultSize()),getDefaultSize()默然返回measureSpec的测量数值,所以继承View进行自定义的wrap_content需要重写。
3342 | * performLayout()会调用最外层的ViewGroup的layout(l,t,r,b),本View在其中使用setFrame()设置本View的四个顶点位置。在onLayout(抽象方法)中确定子View的位置,如LinearLayout会遍历子View,循环调用setChildFrame() --> 子View.layout()
3343 | * performDraw()会调用最外层的ViewGroup的draw()方法,其中会先后调用background.draw()绘制背景,onDraw(绘制自己),dispatchDraw(绘制子View)、onDrawScrollBars(绘制装饰)
3344 | * MeasureSpec由两位SpecMode(UNSPECIFIED、EXACTLY(对于精确值和match_parent)、AL_MOST(对应warp_content))和三十位SpecSize组成一个int,DecorView的MeasureSpec由窗口大小和其LayoutParams决定,其他View有父View的MeasureSpec和本View的LayoutParams决定。ViewGroup中有getChildMeasureSpec()来获取子View的MeasureSpec。
3345 |
3346 | ### 17. 解决滑动冲突的方式?
3347 |
3348 | 在自定义View的过程中经常会遇到滑动冲突问题,一般滑动冲突的类型有三种:(1)外部View滑动方向和内部View滑动方向不一致;(2)外部View滑动方向和内部View滑动方向一致;(3)上述两种情况的嵌套
3349 |
3350 | 一般解决滑动冲突都是利用事件分发机制,有两种方式即外部拦截法和内部拦截法:
3351 |
3352 | **外部拦截法:**
3353 |
3354 | 实现思路是事件首先是通过父容器的拦截处理,如果父容器不需要该事件,则不拦截,将事件传递到子View上面,如果父容器决定拦截的话,则在父容器的onTouchEvent里面直接处理该事件,这种方法符合事件分发机制;具体实现是修改父容器的onInterceptTouchEvent方法,在达到某一条件的时候,让该方法直接返回true,就可以把事件拦截下来进而调用自己的onTouchEvent方法来处理,但是有一点需要注意的是,如果想让子View能够收到事件,我们需要在onInterceptTouchEvent方法里面判断是DOWN事件的话,就返回false,这样后续的MOVE以及UP事件才有机会传递到子View上面,如果你直接在onInterceptTouchEvent方法里面DOWN情况下返回了true,那么后续的MOVE以及UP事件酱由当前View的onTouchEvent处理了,这样的拦截根本没有意义的,拦截只是在满足一定条件下才会拦截,并不是所有情况下都要拦截。
3355 |
3356 | **内部拦截法:**
3357 |
3358 | 实现思路是
3359 |
3360 |
3361 |
3362 | ### 18. APP Build过程
3363 |
3364 | Android Studio点击build按钮之后,AS就会编译整个项目,并将apk安装到手机上,这个过程就是Android工程编译打包过程。主要的流程是:
3365 |
3366 | 编译 --> DEX --> 打包 --> 签名
3367 |
3368 | **APK构建概述**
3369 | 
3370 |
3371 | 主要有两个过程:
3372 |
3373 | * 编译过程
3374 |
3375 | 输入是本工程的文件以及依赖的各种库文件
3376 |
3377 | 输出是dex文件和编译后的资源文件
3378 |
3379 | * 打包过程
3380 |
3381 | 配合Keystore对上述的输出进行签名对齐,生成最终的apk文件
3382 |
3383 | **APK构建步骤详解**
3384 | 
3385 |
3386 | 主要分为六个步骤:
3387 |
3388 | 1. 通过aapt打包res资源文件,生成R.java文件、resource.arsc和res文件(二进制&非二进制如res/raw和pic保持原样)
3389 | 2. 处理.aidl文件,生成对应的Java接口文件
3390 | 3. 通过Java Compiler编译R.java、Java接口文件、Java源文件,生成.class文件
3391 | 4. 通过dex命令,将.class文件和第三方库中的.class文件处理生成classes.dex
3392 | 5. 通过apkbuilder工具,将aapt生成的resource.arsc和res文件、assets文件和classes.dex一起打包生成apk
3393 | 6. 通过Jarsigner工具,对上面的apk进行debug或release签名
3394 | 7. 通过zipalign工具,将签名后的apk进行对齐处理
3395 |
3396 | 参考自:
3397 |
3398 | [https://www.jianshu.com/p/e86aadcb19e0](https://www.jianshu.com/p/e86aadcb19e0)
3399 |
3400 | [http://mouxuejie.com/blog/2016-08-04/build-and-package-flow-introduction/](http://mouxuejie.com/blog/2016-08-04/build-and-package-flow-introduction/)
3401 |
3402 |
3403 |
3404 | ### 19. Android利用scheme协议进行跳转
3405 |
3406 | scheme是一种页面跳转协议。
3407 |
3408 | 通过定义自己的scheme协议,可以非常方便的跳转App中的各个页面;
3409 |
3410 | 通过scheme协议,服务器可以定制化告诉App跳转到App内部页面。
3411 |
3412 | **Scheme协议在Android中的使用场景**
3413 |
3414 | * H5跳转到native页面
3415 | * 客户端获取push消息中后,点击消息跳转到App内部页面
3416 | * App根据URL跳转到另外一个App指定页面
3417 |
3418 | **示例**
3419 | 注册 SchemeActivity:
3420 |
3421 | ```xml
3422 |
3423 |
3424 | //配置协议
3425 |
3426 |
3427 |
3428 |
3429 |
3430 |
3431 | ```
3432 |
3433 | 在MainActivity里通过以下代码跳转到SchemeActivty:
3434 |
3435 | ```java
3436 | String url = "scheme://mtime/goodsDetail?goodsId=2333";
3437 | Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
3438 | startActivity(intent);
3439 | ```
3440 |
3441 | 然后我们可以在SchemeActivity获取到请求的参数等等信息:
3442 |
3443 | ```java
3444 | Uri uri = getIntent().getData();
3445 | assert uri != null;
3446 | String host = uri.getHost();
3447 | String path = uri.getPath();
3448 | String query = uri.getQuery();
3449 | String param = uri.getQueryParameter("goodsId");
3450 | Toast.makeText(this, host + " " + path + " " + query + " " + param, Toast.LENGTH_SHORT).show();
3451 | ```
3452 |
3453 | ### 20. MVC、MVP
3454 |
3455 | **MVC模式**
3456 |
3457 | 
3458 |
3459 | **MVP模式**
3460 |
3461 | 
3462 |
3463 | MVP模式核心思想:
3464 |
3465 | > MVP把Activity中的UI逻辑抽象成View接口,把业务逻辑抽象成Presenter接口,Model类还是原来的Model。
3466 |
3467 | 这就是MVP模式,现在Activity的工作简单了,只是用来相应生命周期,其他工作都丢到Presenter中完成,从上图可以看出,Presenter是Model和View之间的桥梁,为了让结构变得更加简单,View并不能直接对Model进行操作,这也是MVP与MVC最大的不同之处。
3468 |
3469 | **MVP模式的作用**
3470 |
3471 | * 分离了视图逻辑和业务逻辑,降低了耦合
3472 | * Activity只处理生命周期的任务,代码变得更加简洁
3473 | * 视图逻辑和业务逻辑分别抽象到了View和Presenter的接口中去,提高了代码的可阅读性
3474 | * Presenter被抽象成接口,可以有多种具体的实现,所以方便进行单元测试
3475 | * 把业务逻辑抽象到Presenter中去,避免后台线程引用着Activity导致Activity的资源无法被系统回收从而引起内存泄露和OOM
3476 |
3477 | **MVP模式的使用**
3478 | 
3479 |
3480 | 从上述的UML图可以看出,使用MVP至少经历以下几个步骤:
3481 |
3482 | 1. 创建IPresenter接口,把所有业务逻辑的接口都放在这里,并创建它的实现类PresenterCompl(在这里可以方便的查看业务功能,由于接口可以有多种实现所以也方便写单元测试)
3483 | 2. 创建IView接口,把所有视图逻辑的接口都放在这里,其实现类是当前的Activity/Fragment
3484 | 3. 由UML图可以看出,Activity里包含了一个IPresenter,而PresenterCompl里又包含了一个IView并且依赖了Model。Activity里只保留了对IPresenter的调用,其他工作全部留到PresenterCompl中实现
3485 | 4. Model并不是必须有的,但是一定会有View和Presenter
3486 |
3487 |
3488 | **总结**
3489 | MVP模式的整个核心流程:
3490 |
3491 | View与Model并不直接交互,而是使用Presenter作为View与Model之间的桥梁。其实Presenter中同时持有View层的interface的引用以及Model层的引用,而View层持有Presenter层引用。当View层某个界面需要展示某些数据的时候,首先会调用Presenter层的引用,然后Presenter层会调用Model层请求数据,当Model层数据加载成功之后会调用Presenter层的回调方法通知Presenter层数据加载情况,最后Presenter层在调用View层的接口将加载后的数据展示给用户。
3492 |
3493 | 
3494 |
3495 | ### 21. SurfaceView
3496 |
3497 | SurfaceView继承至View的,与View的主要区别在于:
3498 |
3499 | * View主要适用于主动更新的情况下,而SurfaceView主要适用于被动更新,例如频繁的刷新
3500 | * View在主线程中对画面进行更新,而SurfaceView通常会通过一个子线程进行页面的刷新
3501 | * View在绘图时没有使用双缓存机制,而SurfaceView在底层机制中已经实现了双缓存机制
3502 |
3503 | **总结一句话就是:如果自定义View需要频繁的刷新,或者刷新时数据处理量比较大,那么就可以考虑使用SurfaceView来取代View。**
3504 |
3505 | ```java
3506 | /**
3507 | * 用Surface实现画板
3508 | */
3509 | public class DrawBoardView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
3510 |
3511 | private SurfaceHolder mSurfaceHolder;
3512 | private Canvas mCanvas;
3513 | private volatile boolean mIsDrawing;
3514 | private Paint mPaint;
3515 | private Path mPath;
3516 |
3517 | public DrawBoardView(Context context) {
3518 | super(context);
3519 | init();
3520 | }
3521 |
3522 | public DrawBoardView(Context context, AttributeSet attrs) {
3523 | super(context, attrs);
3524 | init();
3525 | }
3526 |
3527 | public DrawBoardView(Context context, AttributeSet attrs, int defStyleAttr) {
3528 | super(context, attrs, defStyleAttr);
3529 | init();
3530 | }
3531 |
3532 | private void init() {
3533 | mSurfaceHolder = getHolder();
3534 | mSurfaceHolder.addCallback(this);
3535 | setFocusable(true);
3536 | setFocusableInTouchMode(true);
3537 | this.setKeepScreenOn(true);
3538 |
3539 | mPaint = new Paint();
3540 | mPaint.setColor(Color.BLUE);
3541 | mPaint.setStyle(Paint.Style.STROKE);
3542 | mPaint.setStrokeWidth(10);
3543 | mPaint.setAntiAlias(true);
3544 | mPath = new Path();
3545 | }
3546 | @Override
3547 | public void surfaceCreated(SurfaceHolder holder) {
3548 | mIsDrawing = true;
3549 | new Thread(this).start();
3550 | }
3551 |
3552 | @Override
3553 | public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
3554 |
3555 | }
3556 |
3557 | @Override
3558 | public void surfaceDestroyed(SurfaceHolder holder) {
3559 | mIsDrawing = false;
3560 | }
3561 |
3562 | @Override
3563 | public void run() {
3564 | long start = System.currentTimeMillis();
3565 | while (mIsDrawing) {
3566 | draw();
3567 | }
3568 | long end = System.currentTimeMillis();
3569 | // 50 - 100
3570 | if (end - start < 100) {//保证线程运行时间不少于100ms
3571 | try {
3572 | Thread.sleep(100 - (end - start));
3573 | } catch (InterruptedException e) {
3574 | e.printStackTrace();
3575 | }
3576 | }
3577 | }
3578 | private synchronized void draw() {
3579 | try {
3580 | mCanvas = mSurfaceHolder.lockCanvas();
3581 | mCanvas.drawColor(Color.WHITE);
3582 | mCanvas.drawPath(mPath,mPaint);
3583 | } catch (Exception e) {
3584 | } finally {
3585 | if (mCanvas != null){
3586 | mSurfaceHolder.unlockCanvasAndPost(mCanvas);
3587 | }
3588 | }
3589 | }
3590 |
3591 | @Override
3592 | public boolean onTouchEvent(MotionEvent event) {
3593 | switch (event.getAction()){
3594 | case MotionEvent.ACTION_DOWN:
3595 | mPath.moveTo(event.getX(),event.getY());
3596 | break;
3597 | case MotionEvent.ACTION_MOVE:
3598 | mPath.lineTo(event.getX(),event.getY());
3599 | break;
3600 | case MotionEvent.ACTION_UP:
3601 |
3602 | break;
3603 | }
3604 | return true;
3605 | }
3606 |
3607 | }
3608 |
3609 | ```
3610 |
3611 |
3612 |
3613 | ### 22. HandlerThread
3614 |
3615 | HandlerThread继承了Thread,所以它本质上是一个Thread,与普通Thread的区别在于,它不仅建立了一个线程,并且创立了消息队列,有自己的looper,可以让我们在自己的线程中分发和处理消息,并对外提供自己这个Looper对象的get方法。
3616 |
3617 | HandlerThread自带的Looper使它可以通过消息队列,来重复使用当前的线程,节省系统资源开销。这是他的优点也是缺点,每一个任务队列都是以队列的方式逐个被执行到,一旦队列中某个任务执行时间过长,那么就会导致后续的任务都会被延时执行。
3618 |
3619 | **使用步骤:**
3620 |
3621 | 1. 创建HandlerThread实例并启动该线程
3622 |
3623 | ```java
3624 | HandlerThread handlerThread = new HandlerThread("myThread");
3625 | handlerThread.start();
3626 | ```
3627 |
3628 | 2. 构造循环消息处理机制
3629 |
3630 | ```Java
3631 | private Handler.Callback mSubCallback = new Handler.Callback() {
3632 | //该接口的实现就是处理异步耗时任务的,因此该方法执行在子线程中
3633 | @Override
3634 | public boolean handleMessage(Message msg) {
3635 | //doSomething 处理异步耗时任务的
3636 | mUIHandler.sendMessage(msg1); //向UI线程发送消息,用于更新UI等操作
3637 | }
3638 | };
3639 | ```
3640 |
3641 | 3. 构造子线程的Handler
3642 |
3643 | ```java
3644 | //由于这里已经获取了workHandle.getLooper(),因此这个Handler是在HandlerThread线程也就是子线程中
3645 | childHandler = new Handler(handlerThread.getLooper(), mSubCallback);
3646 |
3647 | button.setOnClickListener(new OnClickListener() {
3648 | @Override
3649 | public void onClick(View v) {
3650 | //发送异步耗时任务到HandlerThread中,也就是上面的mSubCallback中
3651 | childHandler.sendMessage(msg);
3652 | }
3653 | });
3654 | ```
3655 |
3656 | 4. 构建UI线程Handler处理消息
3657 |
3658 | ```java
3659 | private Handler mUIHandler = new Handler(){
3660 | @Override
3661 | public void handleMessage(Message msg) {
3662 | //在UI线程中做的事情,比如设置图片什么的
3663 | }
3664 | };
3665 | ```
3666 |
3667 | **实现原理**
3668 |
3669 | 我们知道,在子线程创建一个Handler,需要手动创建Looper,即先Looper.parpare(),完了在Looper.loop()。HandlerThread的run方法:
3670 |
3671 | ```java
3672 | @Override
3673 | public void run() {
3674 | mTid = Process.myTid(); //获得当前线程的id
3675 | Looper.prepare();
3676 | synchronized (this) {
3677 | mLooper = Looper.myLooper();
3678 | //发出通知,当前线程已经创建mLooper对象成功,这里主要是通知getLooper方法中的wait
3679 | notifyAll();
3680 | }
3681 | //设置当前线程的优先级
3682 | Process.setThreadPriority(mPriority);
3683 | //该方法实现体是空的,子类可以实现该方法,作用就是在线程循环之前做一些准备工作,当然子类也可以不实现。
3684 | onLooperPrepared();
3685 | Looper.loop();
3686 | mTid = -1;
3687 | }
3688 | ```
3689 |
3690 | **特点:**
3691 |
3692 | 1. HandlerThread其实是Handler+Thread+Looper的组合,它本质上是一个Thread,因为它继承了Thread,相比普通的Thread,他不会阻塞,因为它内部通过Looper实现了消息循环机制,保证了多个任务的串行执行。缺点:效率低。
3693 | 2. HandlerThread需要搭配Handler使用,单独使用的意义不大。HandlerThread线程中作具体的事情,必须要在Handler的callback接口中进行,他自己的run方法被写死了。
3694 | 3. 子线程中的Handler与HandlerThread的联系是通过childHandler=new Handler(handlerThread.getLooper(),mSubCallback)执行的,也就是说,childHandler获得HandlerThread线程的Looper,这样他们两个就在同一个阵营了,这也就是创建Handler作为HandlerThread线程消息执行者,必须在调用start()方法之后的原因 —— HandlerThread.start()之后,run方法才能跑起来,Looper才得以创建,handlerThread.getLooper()才不会出错。
3695 | 4. HandlerThread会将通过handleMessage传递进来的任务进行串行执行,由messageQueue的特性决定的,从而也说明了HandlerThread效率相对比较低。
3696 |
3697 | ### 23. IntentService
3698 |
3699 | IntentService是继承并处理异步请求的一个类,其本质上是一个Service,因为它是继承至Service,所以开启IntentService和普通的Service一致。但是他和普通的Service不同之处在于它可以处理异步任务,在任务处理完之后会自动结束。另外,我们可以启动多次IntentService,而每一个耗时任务会以工作队列的方式在IntentService的onHandleIntent回调方法中执行,并且是串行执行。其实IntentService的内部是通过HandleThread和Handle来实现异步操作的。
3700 |
3701 | ### 24. 谈谈你对Application类的理解
3702 |
3703 | 首先,Application 在一个 Dalvik 虚拟机里面只会存在一个实例。为什么强调说是一个Dalvik虚拟机,而不是一个APP呢?那是因为,一个App有可能有多个Dalvik虚拟机,也就是传说中的多进程模式。在这种模式下,每一个Dalvik都会存在一个Application实例,它们之间没有关系,在A进程Application里面保存的数据不能在B进程的Application获取,因为它们根本不是一个对象。而且被隔离在两个进程里面,所以这里强调的是一个Dalvik虚拟机,而不是一个App。
3704 |
3705 | Application有两个子类,一个是MultiDexApplication,如果你遇到了65535问题,可以选择继承至它,完成相关的工作。另外一个是在TDD(测试用例驱动)开发模式中使用Moke进行测试的时候用到的,可以来替代Application的Moke类MokeApplication。
3706 |
3707 | 在应用启动的时候,会首先调用Application.attach(),当然,这个方法是隐藏的,开发者能接触到的第一个被调用的方法其实是Application.onCreate(),我们通常会在这个方法里面完成各种初始化,比如图片加载库、网络请求库等初始化操作。但是最好别在这个方法里面进行太多的耗时操作,因为这会影响App的启动速度,所以对于不必要的操作可以使用异步操作、懒加载、延时加载等策略来减少对UI线程的影响。
3708 |
3709 | 除此之外,由于在Context中可以通过getApplicationContext()获取到Application对象,或者是通过Activity.getApplication()、Service.getApplication()获取到Application,所以可以在Application保存全局的数据,供所有的Activity或者Service使用。
3710 |
3711 | 使用上面的三种方法获取到的都是同一个Application对象,getApplicationContext()是在Context的实现类ContextImpl中具体实现的,而getApplication()则是在Activity和Service中单独实现的,所以他们的作用域不同,但是获取到的都是同一个Application对象,因为一个Dalvik虚拟机只有一个Application对象。
3712 |
3713 | 但是这里存在一个坑,那就是在低内存情况下,Application有可能被销毁,从而导致保存在Application里面的数据被销毁,最后程序错乱,甚至Crash。
3714 |
3715 | 所以当你想在Application保存数据的时候,请做好判null,或者选择其他方式保存你的数据,比如存储在硬盘上等等。
3716 |
3717 | 另外,在Application中存在几个有用的方法,比如onLowMemory()和onTrimMemory()(Activity里面也存在这两个方法),在这两个方法里面,我们可以实现自己的内存回收逻辑,比如关闭数据库链接、移除图片内存缓存等操作来降低内存消耗,从而降低被系统回收的风险。
3718 |
3719 | 最后,就是要注意Application的生命周期,他和Dalvik虚拟机生命周期一样长,所以在进行单例或者静态变量的初始化操作时,一定要用Application作为Context进行初始化,否则会造成内存泄漏的发生。使用Dialog的时候一般使用Activity的Context。但是也可以使用Application作为上下文,前提是你必须设置Window类型为TYPE_SYSTME_DAILOG,并且申请相关权限。这个时候弹出的Dialog是属于整个Application的,弹出这个Dialog的Activty销毁时也不会回收Dialog,只有在Application销毁时,这个Dialog才会自动消失。
3720 |
3721 |
3722 |
3723 | ### 25. Android为什么要设计出Bundle而不是直接使用HashMap来进行数据传递?
3724 |
3725 | - Bundle内部是由ArrayMap实现的,ArrayMap的内部实现是两个数组,一个int数组是存储对象数据对应下标,一个对象数组保存key和value,内部使用二分发对key进行排序,所以在添加、删除、查找数据的时候,都会使用二分发查找,只适用于小数据量操作,如果在数据量比较大的情况下,那么它的性能将退化。而HashMap内部则是数组+链表结构,所以在数据量较小的时候,HashMap的Entry Array比ArrayMap占用更多的内存。因为使用Bundle的场景大多数为小数据量,所以相比之下,在这种情况下使用ArrayMap保存数据,在操作速度和内存占用上都具有优势。
3726 | - 另外一个原因,则是在Android中如果使用Intent来携带数据的话,需要数据是基本类型或者是可序列化类型,HashMap使用Serializable进行序列化,而Bundle则是使用Pracelable进行序列化。而在Android平台,更推荐使用Pracelable实现序列化,虽然写法复杂,但是开销更小,所以为了更加快速的进行数据的序列化和反序列化,系统封装了Bundle类,方便我们进行数据的传输。
3727 |
3728 |
3729 |
3730 | ### 26. SharedPreference在使用过程中有什么注意点?
3731 |
3732 | **commit()和apply()的区别**
3733 |
3734 | - 返回值
3735 |
3736 | apply是没有返回值的,而commit返回boolean表明修改是否提交成功。
3737 |
3738 | - 操作效率
3739 |
3740 | apply是将修改数据提交到内存,而后异步真正提交到硬盘磁盘,而commit是同步的提交到硬盘磁盘上。
3741 |
3742 | 因此在多并发commit的时候,会等待正在处理的commit保存到磁盘后再操作,从而降低了效率。而apply只是原子的提交到内存,后面有调用apply的函数的将会直接覆盖前面的内存数据,从一定程度上提高了效
3743 |
3744 | - 建议
3745 |
3746 | 如果对提交的结果不关心的话,建议使用apply,如果需要确保提交成功且有后续操作的话,还是使用commit。
3747 |
3748 | **多进程表现**
3749 |
3750 | 在多进程中,如果要交换数据,不用使用SharedPreference,因为不同版本表现不稳定,推荐使用ContentProvider替代。
3751 |
3752 | 在有的文章中,有提到在多进程中使用SharePrefenerce添加标志位(MODE_MULTI_PROCESS)就可以了。这个标志位是2.3(API 9)之前默认支持的,但是在2.3之后,需要多进程的访问的情景,就需要显示的声明出来。
3753 |
3754 | 现在这个标志位被废弃了,因为在某些版本上表现不稳定。
3755 |
3756 |
3757 |
3758 | ### 27. SQLite有哪些可以优化的地方?
3759 |
3760 | - 创建索引
3761 |
3762 | 索引有助于加快SELECT查询和WHERE子句,但是会减慢使用UPDATE和INSERT语句时的数据输入。索引可以创建或删除,但不会影响数据。
3763 |
3764 | 优点是加快了数据库检索的速度,包括对单表查询、分组查询、排序查询。
3765 |
3766 | 缺点是索引的创建和维护存在消耗,索引会占用物理内存,且随着数据量的增加而增加。在对数据库进行增删改时需要维护索引,所以会对增删改的性能存在影响。
3767 |
3768 | 使用场景:当某字段数据更新频率较低,查询频率较高,经常有范围查询(> < = >= <=)或order by、group by 发生时建议使用索引。并且选择度越大,建立索引越有优势,这里选择度指一个字段中唯一值的数量/总的数量。还有就是经常同时存取多列,且每列都含有重复值可考虑建立复合索引。
3769 |
3770 | - 使用事务
3771 |
3772 | 原子性操作,要么全部成功,要么全部失败;在执行大量数据的插入操作时,避免频繁操作cursor,可以大幅减少insert操作时间,一般为1-2个数量级。
3773 |
3774 | - 查询必要字段
3775 |
3776 | 查询时只取需要的字段和结果集,更多的结果集会消耗更多的时间以及内存,更多的字段会导致更多的内存消耗。
3777 |
3778 | ### 28. 嵌滑滑动机制
3779 |
3780 | 我们知道,Android的时间分发机制中,只要有一个控件消费了事件,其他控件就没办法在接受到这个事件了。因此,当有嵌套滑动场景时,我们需要自己动手解决事件冲突。
3781 |
3782 | 嵌套滑动机制的基本原理可以认为是事件共享,即当子控件接受到滑动事件,准备要滑动时,会先通知父控件(startNestedScroll);然后在滑动之前,会先询问父控件是否要滑动(dispatchNestedPreScroll);如果父控件响应该事件进行了滑动,那么就会通知子控件它具体消耗了多少滑动距离;然后交由子控件处理剩余的滑动距离;最后子控件滑动结束后,如果滑动距离还有剩余,就会在问一下父控件是否需要在继续滑动剩下的距离(dispatchNestedScroll)
3783 |
3784 | ### 29. RecyclerView 优化
3785 |
3786 | 1. 尽量减少Item布局嵌套,比如使用ConstraintLayout
3787 |
3788 | 2. 可以的话,写死ItemView的布局宽高,然后设置RecyclerView.setHasFixedSize(true)。当知道Adapter内Item的改变不会影响RecyclerView的宽高时,这样做可以避免重新计算大小。
3789 |
3790 | 但是,如果调用notifyDataSetChanged(),大小还是会重新计算(requestLayout)。当调用Adapter的增删改查方法,最后就会根据mHasFixedSize这个值来判断需不需要requestLayout()。
3791 |
3792 | 3. 根据需求修改RecyclerView默认的绘制缓存选项
3793 |
3794 | recyclerView.setItemViewCacheSize(20)
3795 |
3796 | recyclerView.setDrawingCacheEnabled(true)
3797 |
3798 | recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH)
3799 |
3800 | 4. 在onBindViewHolder 或 getView 方法中,减少逻辑判断,减少临时对象创建。例如:复用事件监听,在其方法外部创建监听,可以避免生成大量的临时变量。
3801 |
3802 | 5. 避免整个列表的数据更新,只更新受影响的布局。例如,加载更多时,不使用notifyDataSetChanged,而是使用notifyItemRangeInserted(rangeStart,rangeEnd)
3803 |
3804 |
3805 |
3806 | ## Android 部分(二)高级知识点
3807 |
3808 | ### 1. Android 系统架构
3809 |
3810 | - 应用层
3811 | - 应用框架层
3812 | - 系统运行库层
3813 | - 硬件抽象层
3814 | - Linux 内核层
3815 |
3816 | 
3817 |
3818 |
3819 |
3820 | ### 2. Android 各个版本特性
3821 |
3822 | 1. Android 5
3823 |
3824 | 引入 Material Design、ART、优化通知栏、弃用 HTTP 类、增强 WebView、TLS / SSL 默认配置变更。
3825 |
3826 | 2. Android 6
3827 |
3828 | 运行时权限、低电耗模式、应用待机模式、指纹身份验证。
3829 |
3830 | 3. Android 7
3831 |
3832 | SurfaceView、更多的表情支持、多窗口支持、通知增强功能。
3833 |
3834 | 4. Android 8
3835 |
3836 | 后台执行限制、后台位置限制、画中画模式、可下载字体、自动调整 TextView 的大小、自适应图标、新增权限。
3837 |
3838 | 5. Android 9
3839 |
3840 | 对使用非 SDK 接口的限制、电池管理、强制执行 FLAG_ACTIVITY_NEW_TASK 要求、利用 WI-FI 进行室内定位、全面屏支持、适用于可绘制图像和位图的 ImageDecoder、引入 AnimatedImageDrawable 类,用于绘制和显示 GIF 和 WebP 图像。
3841 |
3842 | 参考:[Android 各版本特性](https://www.jianshu.com/p/e8db7954a895)
3843 |
3844 | ### 3. Android 系统启动流程
3845 |
3846 | 1. init 进程启动过程
3847 | 1. 创建和挂载启动所需的文件目录
3848 | 2. 初始化和启动属性服务
3849 | 3. 解析 init.rc 配置文件并启动 Zygote 进程
3850 | 2. Zygote 进程启动过程
3851 | 1. 创建 AppRuntime 并调用 start 方法,启动 Zygote 进程
3852 | 2. 创建 Java 虚拟机并为 Java 虚拟机注册 JNI 方法
3853 | 3. 通过 JNI 调用 ZygoteInit 的 main 方法进入 Zygote 的 Java 框架层
3854 | 4. 通过 registerZygoteSocket 方法创建服务端 Socket,并通过 runSelectLoop 方法等待 AMS 的请求来创建新的应用程序进程
3855 | 5. 启动 SystemServer 进程
3856 | 3. SystemServer 处理过程
3857 | 1. 启动 Binder 线程池,这样就可以与其他进程进行通信
3858 | 2. 启动 SystemServiceManager,其用于对系统的服务进程创建、启动和生命周期管理
3859 | 3. 启动各种启动服务
3860 | 4. Launcher 启动过程
3861 |
3862 | 
3863 |
3864 |
3865 |
3866 | ### 4. 应用程序进程启动过程
3867 |
3868 | 启动过程可以分为两步:
3869 |
3870 | 1. AMS 发送启动应用程序进程请求
3871 |
3872 | AMS 如果想要启动应用程序进程,就需要向 Zygote 进程发送创建应用程序进程的请求,AMS 会通过调用 startProcessLocked 方法向 Zygote 进程发送请求。
3873 |
3874 | 2. Zygote 接收请求并创建应用程序进程
3875 |
3876 | ### 5. Activity 状态的保存与恢复
3877 |
3878 | 其实就是 onSaveInstanceState 和 onRestoreInstanceState 方法的使用。不过需要注意的是 onRestoreInstanceState 方法时,应当先调用 super 方法,这样由系统负责保存的部分才能够恢复,比如文本输入类型控件的输入文本以及光标位置。
3879 |
3880 | 整个保存 View 状态的流程如下:
3881 |
3882 | 1. 调用 Activity 的 onSaveInstanceState 方法
3883 | 2. 该方法又调用 mWindow.saveHierarchyState,把返回的结果保存到 WINDOW_HIERARCHY_TAG 这个 key 对应的 value 中
3884 | 3. mWindow 的实现类 PhoneWindow 当中,调用根布局的 saveHierarchyState 方法,这里面会从根布局按树形结构遍历,调用每个 ViewGroup / View 的 onSaveInstanceState。
3885 |
3886 | 于是,我们得出结论,保存的前提有两个:
3887 |
3888 | 1. View 的子类必须实现了 onSaveInstanceState
3889 | 2. 它必须要有一个 ID,这个 ID 作为 Bundle 的 key,这也为我们实现自定义 View 时,需要保存状态提供了思路。
3890 |
3891 | onSaveInstanceState 调用时机:
3892 |
3893 | 在 onPause() 方法之后,和 onStop() 方法没有既定的时序关系。
3894 |
3895 | onRestoreInstanceState 调用时机:
3896 |
3897 | 在 onStart() 方法之后,onResume() 之前。
3898 |
3899 | ### 6. Android 动画框架实现原理
3900 |
3901 | ### 7. requestLayout、onLayout、onDraw、drawChild 区别与联系
3902 |
3903 | ### 8. requestLayout、invalidate、postInvalidate 的区别
3904 |
3905 | 1. requestLayout 会回掉 onMeasure、onLayout、onDraw(ViewGroup.setWillNotDraw(fasle)情况下)方法
3906 | 2. invalidate 只会回掉 onDraw 方法
3907 | 3. postInvalidate 只会回掉 onDraw 方法(可以在非 UI 线程中调用)
3908 |
3909 | ### 9. Activity、Window、View 的区别以及联系
3910 |
3911 | ### 10. Volley 的理解
3912 |
3913 | ### 11. 如何优化自定义 View
3914 |
3915 | ### 12. 低版本如何实现高版本 API
3916 |
3917 | ### 13. 描述一次网络请求的过程
3918 |
3919 | ### 14. HttpUrlConnection 与 OkHttp 的关系
3920 |
3921 | ### 15. Bitmap 的理解
3922 |
3923 | ### 16. Looper 架构
3924 |
3925 | ### 17. ActivityThread 的工作原理
3926 |
3927 | ### 18. AMS 的工作原理
3928 |
3929 | ### 19. WMS 的工作原理
3930 |
3931 | ### 20. 自定义 View 如何考虑机型适配
3932 |
3933 | ### 21. 自定义 View 的事件
3934 |
3935 | ### 22. LaunchMode 应用场景
3936 |
3937 | ### 23. SpareArray 原理
3938 |
3939 | ### 24. ContentProvider 是如何实现数据共享的
3940 |
3941 | ### 25. Service 与 Activity 的通信方式
3942 |
3943 | ### 26. IntentService 原理与作用
3944 |
3945 | ### 27. ApplicationContext 与 ActivityContext 的区别
3946 |
3947 | ### 28. SP 是进程同步的嘛?如何做到进程同步?
3948 |
3949 | ### 29. 谈谈多线程在 Android 中的应用
3950 |
3951 | ### 30. 进程和 Application 的生命周期
3952 |
3953 |
3954 |
3955 |
3956 | ## 数据结构
3957 |
3958 | * 线性表
3959 | * 栈和队列
3960 | * 树
3961 | * 图
3962 | * 散列查找
3963 | * 排序
3964 | * 海量数据处理
3965 |
3966 | #### 线性表
3967 |
3968 | ##### 概述
3969 |
3970 | 线性表是一种线性结构,它是具有相同类型的n(n>=0)个数据元素组成的有限序列。本章先介绍线性表的几个基本组成部分:数组、单向链表、双向链表。
3971 |
3972 | **数组**
3973 |
3974 | 数组有上界和下界,数组的元素在上下界内是连续的。
3975 |
3976 | 存储10,20,30,40,50的数组的示意图如下:
3977 |
3978 | 
3979 |
3980 | 数组的特点是:
3981 |
3982 | 数据是连续的;随机访问速度快。数组中稍微复杂一点的是多维数组和动态数组。对于C语言而言,多维数组本质上也是通过一维数组实现的。至于动态数组,是指数组的容量能动态增长的数组;对于Java而言,Collection集合中提供了ArrayList和Vector。
3983 |
3984 | **单向链表**
3985 |
3986 | 单链表是链表的一种,它由节点组成,每个节点都包含下一个节点的指针。
3987 |
3988 | 
3989 |
3990 | 表头为空,表头的后继节点是“节点10”(数据是10的节点),“节点10”的后继节点是“节点20”(数据为20的节点)......
3991 |
3992 | 单链表删除节点:
3993 |
3994 | 
3995 |
3996 | 单链表添加节点:
3997 |
3998 | 
3999 |
4000 | 单链表的特点是:
4001 |
4002 | 节点的链接方向是单向的;相对于数组来说,单链表的随机访问速度较慢,但是单链表删除 / 添加数据的效率很高。
4003 |
4004 | **双向链表**
4005 |
4006 | 双向链表是链表的一种,和单链表一样,双链表也是由节点组成,它的每个数据节点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个节点开始,都可以很方便的访问它的前驱结点和后继结点,一般我们都构造双向循环链表。
4007 |
4008 | 
4009 |
4010 | 双链表删除节点:
4011 |
4012 | 
4013 |
4014 | 双链表添加节点:
4015 |
4016 | 
4017 |
4018 | **总结**
4019 |
4020 | 链表(LinkedList)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存下一个节点的指针,简单来说链表并不像数组那样将数组存储在一个连续的内存地址空间里,它们可以不是连续的因为他们每个节点保存着下一个节点的引用地址,所以较数组来说是一个优势。
4021 |
4022 | * 链表增删元素的时间复杂度为O(1),查找一个元素的时间复杂度为O(n)
4023 | * 单链表不用像数组那样预先分配存储空间的大小,避免了空间浪费
4024 | * 单链表不能进行回溯操作
4025 |
4026 | **单链表的基本操作**
4027 |
4028 | 获取单链表的长度:
4029 |
4030 | 由于单链表的存储地址是不连续的,链表并不具有直接获取链表长度的功能,对于一个链表的长度我们只能依次去遍历链表的节点,直到找到某个节点的下一个节点为空的时候得到链表的总长度。注意这里的出发点并不是一个空链表然后依次添加节点后,然后去读取已经记录的节点个数,而是已知一个链表的头节点然后去获取这个链表的长度。
4031 |
4032 | ```java
4033 | public int getLength(Node head){
4034 |
4035 | if(head == null){
4036 | return 0;
4037 | }
4038 |
4039 | int len = 0;
4040 | while(head != null){
4041 | len++;
4042 | head = head.next;
4043 | }
4044 | return len;
4045 | }
4046 | ```
4047 |
4048 | 查询指定索引的节点值或指定值的节点值的索引:
4049 |
4050 | 由于链表是一种非连续性的存储结构,节点的内存地址不是连续的,也就是说链表不能像数组那样可以通过索引值获取索引元素的位置。所以链表的查询的时间复杂度要是O(n)级别的,这点和数组查询指定值的元素位置是相同的,因为你要查找的东西在内存中的存储地址都是不一定的。
4051 |
4052 | ```java
4053 | /** 获取指定角标的节点值 */
4054 | public int getValueOfIndex(Node head, int index) throws Exception {
4055 |
4056 | if (index < 0 || index >= getLength(head)) {
4057 | throw new Exception("角标越界!");
4058 | }
4059 |
4060 | if (head == null) {
4061 | throw new Exception("当前链表为空!");
4062 | }
4063 |
4064 | Node dummyHead = head;
4065 |
4066 | while (dummyHead.next != null && index > 0) {
4067 | dummyHead = dummyHead.next;
4068 | index--;
4069 | }
4070 |
4071 | return dummyHead.value;
4072 | }
4073 |
4074 |
4075 | /** 获取节点值等于 value 的第一个元素角标 */
4076 | public int getNodeIndex(Node head, int value) {
4077 |
4078 | int index = -1;
4079 | Node dummyHead = head;
4080 |
4081 | while (dummyHead != null) {
4082 | index++;
4083 | if (dummyHead.value == value) {
4084 | return index;
4085 | }
4086 | dummyHead = dummyHead.next;
4087 | }
4088 |
4089 | return -1;
4090 | }
4091 | ```
4092 |
4093 | **链表添加一个元素**
4094 |
4095 | 链表的插入操作分为头插法、尾插法和随机节点插入法。当然数据结构讲的时候也是针对一个已经构造好的(保存了链表头部节点和尾部节点引用)
4096 |
4097 |
4098 |
4099 | ## 计算机网络
4100 |
4101 | ### 1. TCP 和 UDP 的区别
4102 |
4103 | TCP:
4104 |
4105 | 传输控制协议,提供的是面向连接、可靠的字节流服务,传输数据前经过三次握手建立连接,保证数据传输的可靠性,但效率比较低,一般用于数据传输安全性较高的场合。
4106 |
4107 | UPD:
4108 |
4109 | 用户数据报协议,是一个简单的面向数据报的运输层协议,面向无连接。UDP不提供可靠性,数据传输可能发生错序,丢包,但效率1较高,一般用于对于实时性要求较高的场合。
4110 |
4111 | **总结:**
4112 |
4113 | * UDP在传送数据之前不需要先建立连接。远程主机运输层在收到UDP报文后,不需要给出任何确认。因此UDP不提供可靠交付,但是效率高。TCP则提供面向连接的服务,在传送数据之前必须先建立连接,数据传送结束后要释放连接。TCP要提供可靠的、面向连接的运输服务,因此不可避免的增加了许多的开销,如确认、流量控制等等。
4114 | * TCP和UDP在发送报文时所采用的方式完全不同。TCP并不关心进程一次把多长的报文发送到TCP的缓存中,而是根据对方给出的窗口值和当前网络拥塞程度决定一个报文段包含多少字节,而UDP发送报文长度是应用进程给出的。如果应用进程传送到TCP缓存的数据块太长,TCP就划分短一些在传送。若过短也可以等待积累足够多的字节后再构成报文段发送出去。
4115 | * UDP程序结构比较简单,它的首部最少为8个字节而TCP最少为20字节。
4116 | * UDP不保证数据的顺序结构,而TCP必须保证数据的顺序结构。
4117 | * TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的,UDP没有阻塞控制,因此网络出现阻塞不会使源主机的发送速率降低。
4118 |
4119 | ### 2. Http1.1和Http1.0及2.0的区别
4120 |
4121 | **HTTP1.1与1.0的区别**
4122 |
4123 | * 长连接
4124 | HTTP1.0协议使用非持久连接,即在非持久连接下,一个TCP连接只传输一个Web对象。
4125 |
4126 | HTTP1.1支持持久连接,也就是说长连接,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。
4127 |
4128 | 一个包含有许多图像的网页文件的多个请求和应答可以在一个连接中传输,但每个单独的网页文件的请求和应答仍然需要使用各自的连接。HTTP1.1还允许客户端不要等待上一次请求结果返回,就可以发出下一次请求,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容,这样就显著的减少了整个下载过程所需要的时间。
4129 |
4130 | HTTP1.0需要使用keep-alive参数来告知服务器端要建立一个长连接,而HTTP1.1默认支持长连接。HTTP是基于TCP/IP协议的,创建一个TCP连接是需要经过三次握手的,有一定的开销,如果每次通讯都要重新建立连接的话,对性能有影响。因此最好能维持一个长连接。
4131 |
4132 | * 缓存
4133 |
4134 | * 节约带宽
4135 |
4136 | HTTP/1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象传送过来。例如,客户端只需要显示一个文档的部分内容,又比如下载大文件需要支持断点续传,而不是发送断连后不得不重新下载完整的包。
4137 |
4138 | HTTP1.1中在请求消息中引入了range头域,他只允许请求资源的某个部分。在响应消息中Content-Range头域声明了返回的这部分对象的偏移值和长度。如果服务器响应的返回了对象所请求范围的内容,则响应码为206,它可以防止Cache将响应误以为是一个完整的对象。
4139 |
4140 | 节省带宽资源的一个非常有效的做法就是压缩要传送的数据。
4141 |
4142 | * Host域
4143 |
4144 | * 错误提示
4145 |
4146 |
4147 | **HTTP1.1与HTTP2.0的区别**
4148 |
4149 | * 多路复用
4150 |
4151 | HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。
4152 |
4153 | 当然HTTP1.1也可以多建立几个TCP连接,来支持处理更多并发的请求,但是创建TCP连接本身也是有开销的。
4154 |
4155 | TCP连接有一个预热和保护的过程,先检查数据是否传送成功,一旦成功过后,则会慢慢增加传输速度,因此对应瞬时并发的链接,服务器的响应就会变慢。所以最好能使用一个建立好的连接,并且这个连接支持瞬时并发的请求。
4156 |
4157 | * 数据压缩
4158 |
4159 | 我们知道,HTTP请求和响应都是由状态行、请求/响应头部,消息主体三部分组成的。一般而言,消息主体都会经过gzip压缩,或者本身传输的就是压缩过后的二进制文件(如图片、音频等),但是状态行和头部多是没有经过任何压缩,而是以纯文本的方式进行传输的。
4160 |
4161 | 然而,随着web功能越来越多,请求数量越来越多,随之而来的就是头部的流量越来越多,并且在建立初次连接之后的链接也要发送User-Agent等信息,实在是一种浪费,因此,HTTP2.0提出了对请求和响应的头部进行压缩,而不只是压缩主体部分,这种压缩方式就是HAPCK。
4162 |
4163 | * 服务器推送
4164 |
4165 | 为了改善延迟,HTTP2.0引入了Server Push,它允许服务端推送资源给浏览器,在浏览器明确的请求之前,一个服务器经常知道一个页面需要很多附加资源,在它响应浏览器第一个请求的时候,就可以开始推送这些资源。这允许服务端充分的利用一个可能空闲的网络,改善页面加载时间。
4166 |
4167 |
4168 | ### 3. DNS解析过程
4169 |
4170 | [https://www.jianshu.com/p/7e268c559aff](https://www.jianshu.com/p/7e268c559aff)
4171 |
4172 | DNS域名解析服务器。解析过程:
4173 |
4174 | 1. 当用户在浏览器输入一个域名的时候,最先浏览器会从自己的缓存中寻找指定的结果。如果找到了域名对应的IP则域名解析完成。这个缓存大小是有限的,另外每一条结果都有过期时间,这个过期时间通过TTL属性来指定。
4175 |
4176 | 2. 如果在浏览器中的缓存没有命中,则会在系统的缓存中查找这个域名是否有对应的DNS解析结果,如果有则域名解析完成。这个缓存通常是以文件的方式来保存,比如windows下通过C:\windows\system32\driver\etc\hosts文件来设置的,Linux则是etc/named/config文件,通过编辑这个文件我们能把域名映射到任意一个IP中。
4177 |
4178 | > 如果前面两个流程都没有找到指定域名的解析结果,那么下面就要进行真正的域名解析了。为什么叫做真正的域名解析呢?因为前面的都是在本机中完成的,下面的流程就要依赖外部服务器来查找指定的域名的解析结果
4179 |
4180 | 3. 系统缓存未命中之后会把这个域名提交到指定的LDNS服务器中(本地DNS服务器),这个服务器就是你计算机设定的DNS服务器。如果你在学校的网络中,这个DNS服务器一定在你学校里,如果你在小区的网络,这个DNS服务器通常是运营商提供的。这个域名解析服务器缓存了大量的域名的DNS解析结果,通常80%的DNS解析需求在这一步就满足了,所以LDNS完成了大部分的DNS解析任务。
4181 |
4182 | 4. 如果指定的域名在LDNS服务器的缓存中仍然没有命中,LDNS会向ROOT Service发送请求。
4183 |
4184 | 5. ROOT Server会返回给LDNS一个指定域名对应的主域名服务器gTL的地址D,gTLD是顶级域名服务器,如com、cn、org等等。
4185 |
4186 | 6. LDNS接下来会向这个gTLD服务器发送域名解析请求。
4187 |
4188 | 7. 接收请求的gTLD会返回给LDNS一个该域名对应的Name Server服务器地址,这个Name Server通常就是你注册的域名服务器。例如你在某个域名服务提供商申请的域名,这个域名就由他们的服务器解析。
4189 |
4190 | 8. NAME Server会把指定域名的IP和一个TTL返回给LDNS。
4191 |
4192 | 9. LDNS会把这个结果缓存下来,缓存的过期时间由TTL来决定
4193 |
4194 | 10. 然后LDNS再把这个结果返回给用户,DNS解析结束。
4195 |
4196 | 从上面来看如果LDNS中没有查找到指定域名的对应IP,则需要很长的时间来获取解析结果。但是一旦解析结果被缓存了,下次在请求同样的域名就不会那么慢了。
4197 |
4198 | ### 4. HTTP请求报文和响应报文
4199 |
4200 | [HTTP请求报文和响应报文](http://www.cnblogs.com/biyeymyhjob/archive/2012/07/28/2612910.html)
4201 |
4202 | HTTP报文是面向文本的,报文中的每一个字段都是一些ASCII码串,各个字段的长度是不确定的。HTTP有两类报文:请求报文和响应报文。
4203 |
4204 | **HTTP请求报文**
4205 |
4206 | 一个HTTP请求报文由请求行、请求头、空行和请求数据四个部分组成。
4207 |
4208 | 
4209 |
4210 | * 请求行
4211 |
4212 | 请求行由请求方法、URL字段、HTTP协议这三个字段组成,它们用空格分隔。例如:GET /index.html HTTP/1.1
4213 |
4214 | * 请求头部
4215 |
4216 | 请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号分隔。请求头部通知服务器有关于客户端请求的信息,典型的请求头有:
4217 |
4218 | User-Agent:产生请求的浏览器类型。
4219 |
4220 | Accept:客户端可识别的内容类型列表。
4221 |
4222 | Host:请求的主机名,允许多个域名同一个IP地址,即虚拟主机。
4223 |
4224 | * 空行
4225 |
4226 | 最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头。
4227 |
4228 | * 请求数据
4229 |
4230 | 请求数据不在GET方法中使用,而是在POST方法中使用。POST方法适用于需要客户端填写表单的场合。与请求数据相关的最常用的请求头是Content-Type和Content-Length。
4231 |
4232 | **HTTP响应报文**
4233 |
4234 | HTTP响应报文也是由三个部分组成,分别是:状态行、请求包头、响应正文。类似以下:
4235 |
4236 | ```
4237 | <status-line>
4238 |
4239 | <headers>
4240 |
4241 | <blank line>
4242 |
4243 | [<response-body>]
4244 | ```
4245 |
4246 | 在相应中唯一真正的区别在于第一行中用状态信息代替了请求信息。状态行通过提供一个状态码来说明请求的资源情况。
4247 |
4248 | 状态行格式如下:
4249 |
4250 | ```
4251 | HTTP-Version Status-Code Reason-Phrase CRLF
4252 | ```
4253 |
4254 | 其中,HTTP-Version表示服务器HTTP协议的版本;Status-Code表示服务器发回的响应状态代码;Reason-Phrase表示状态代码的文本描述。状态码由三位数字组成,第一个数字定义了响应的类别,且有五种可能的取值。
4255 |
4256 | 1. 1xx:指示信息,表示请求已接收,继续处理
4257 | 2. 2xx:成功,表示请求已被成功接收
4258 | 3. 3xx:重定向,要完成的请求必须进行更进一步的操作
4259 | 4. 4xx,客户端错误,请求有语法错误或者请求无法实现
4260 | 5. 5xx,服务器端错误,服务器未能实现合法的请求
4261 |
4262 | 常见的状态码:
4263 |
4264 | 100:客户端必须继续发出请求
4265 |
4266 | 101:客户端要求服务器根据请求转换HTTP协议版本
4267 |
4268 | 200:响应成功
4269 |
4270 | 206:客户端表明自己只需要目标URL上的部分资源的时候返回的。
4271 |
4272 | 301:永久重定向
4273 |
4274 | 302:临时重定向
4275 |
4276 | 503:服务不可用
4277 |
4278 | 504:网关超时
4279 |
4280 | HTTP响应报文的例子:
4281 |
4282 | ```xml
4283 | HTTP/1.1 200 OK
4284 | Date: Sat, 31 Dec 2005 23:59:59 GMT
4285 | Content-Type: text/html;charset=ISO-8859-1
4286 | Content-Length: 122
4287 |
4288 | <html>
4289 | <head>
4290 | <title>Wrox Homepage</title>
4291 | </head>
4292 | <body>
4293 | <!-- body goes here -->
4294 | </body>
4295 | </html>
4296 | ```
4297 |
4298 | ### 5. 简述HTTP
4299 |
4300 | HTTP协议即超文本传输协议,是用于从万维网服务器传输超文本到本地浏览器的传送协议。HTTP是一个基于TCP/IP通信协议来传递数据。HTTP是一个属于应用层的面向对象的协议,由于其简单、快速的方式,适用于分布式超媒体信息系统。
4301 |
4302 | **工作原理**
4303 | HTTP协议工作于客户端-服务端架构上。浏览器作为HTTP客户端通过URL向HTTP服务器即WEB服务器发送请求,WEB服务器接收请求后,向客户端发送响应信息。HTTP三点注意事项:
4304 |
4305 | * 简单快速灵活:客户端向服务器端发送请求后,只需要传送请求方法和路径,HTTP允许传输任意类型的数据对象,正在传输的类型由Content-Type加以标记。
4306 |
4307 |
4308 | * HTTP是无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户端的请求,并收到客户端应答后,即断开连接。采用这种方式可以节省传输时间。
4309 | * HTTP是媒体独立的:这意味着,只要客户端和服务器知道如何处理的数据内容,任何类型的数据都可以通过HTTP发送。客户端以及服务器指定使用合适的MIME-type内容类型。
4310 | * HTTP是无状态:HTTP协议是无状态协议,无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另外一方面,在服务器不需要先前信息时它的应答就较快。
4311 |
4312 |
--------------------------------------------------------------------------------
/JVM/JVM 面试题集.md:
--------------------------------------------------------------------------------
1 | ---
2 | JVM 面试题集
3 | ---
4 |
5 | 1. JVM 运行时内存的结构
6 | 2. Java 堆和栈的区别
7 | 3. 内存泄露和内存溢出
8 | 4. Java 的四种引用
9 | 5. GC 如何判断对象死活?
10 | 6. GCRoot 有哪些?
11 | 7. GC 回收算法
12 | 8. MinorGC 和 FullGC
13 | 9. 内存分配策略
14 | 10. GC 垃圾收集器
15 | 11. Java 类加载机制,哪些情况能触发类加载?
16 | 12. Java 对象结构及其创建过程,如何定位对象?
17 | 13. JVM 编译优化有了解吗?
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android_QA
2 |
3 | ## Java 部分(一)基础知识点
4 |
5 | 1. [Java中 == 和 equals 和 hashCode 的区别](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_1)
6 | 2. [int、char、long 各占多少字节数](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_2)
7 | 3. [int 和 Integer 的区别](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_3)
8 | 4. [谈谈对Java多态的理解](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_4)
9 | 5. [String、StringBuffer、StringBuilder的区别](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_5)
10 | 6. [什么是内部类?内部类的作用](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_6)
11 | 7. [抽象类和接口的区别](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_7)
12 | 8. [抽象类的意义](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_8)
13 | 9. [抽象类与接口的应用场景](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_9)
14 | 10. [抽象类是否可以没有方法和属性?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_10)
15 | 11. [接口的意义](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_11)
16 | 12. [泛型中的extends和super的区别](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_12)
17 | 13. [父类的静态方法能否被子类重写?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_13)
18 | 14. [进程和线程的区别](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_14)
19 | 15. [final、finally、finalize的区别](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_15)
20 | 16. [序列化的方式](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_16)
21 | 17. [Serializable 和 Parcelable 的区别](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_17)
22 | 18. [静态属性和静态方法是否可以被继承?是否可以被重写?以及原因。](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_18)
23 | 19. [静态内部类的设计意图](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_19)
24 | 20. [成员内部类、静态内部类、方法内部类(局部内部类)和匿名内部类的理解,以及项目中的应用](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_20)
25 | 21. [谈谈对kotlin的理解](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_21)
26 | 22. [闭包和局部内部类的区别](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_22)
27 | 23. [String转换成Integer的方式以及原理](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_23)
28 | 24. [面向对象思想](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_24)
29 | 25. [对象拷贝的理解?深拷贝、浅拷贝的区别?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_25)
30 | 26. [Enumeration 和 Iterator 的区别?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_base_26)
31 | 27.
32 |
33 |
34 |
35 | ## Java 部分(二)高级知识点
36 |
37 | 1. [哪些情况下的对象会被垃圾回收机制处理掉?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_1)
38 | 2. [讲一下常见的编码方式?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_2)
39 | 3. [UTF-8编码中中文占几个字节,int型几个字节?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_3)
40 | 4. [静态代理和动态代理的区别,什么场景使用?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_4)
41 | 5. [Java的异常体系](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_5)
42 | 6. [谈谈你对解析与分派的认识](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_6)
43 | 7. [修改对象A的equals方法的签名,那么使用HashMap存放这个对象实例的时候,会调用哪个equals方法?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_7)
44 | 8. [Java中实现多态的机制是什么?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_8)
45 | 9. [如何将一个Java对象序列化到文件里?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_9)
46 | 10. [说说你对Java反射的理解](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_10)
47 | 11. [说说你对Java注解的理解](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_11)
48 | 12. [说说你对依赖注入的理解](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_12)
49 | 13. [说一下泛型原理,并举例说明](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_13)
50 | 14. [ Java中String的了解](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_14)
51 | 15. [String为什么要设计成不可变的?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_15)
52 | 16. [Object类的equal和hashCode方法重写,为什么?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_16)
53 | 17. [为什么Java里的匿名内部类只能访问final修饰的外部变量?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_17)
54 | 18. [Synchronized](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_18)
55 | 19. [volatile](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_19)
56 | 20. [类加载流程双亲委托机制](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_advance_20)
57 |
58 |
59 |
60 | ## Java 部分(三)数据结构
61 |
62 | 1. [常用数据结构简介](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_structure_1)
63 | 2. [并发集合了解哪些?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_structure_2)
64 | 3. [HashMap](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_structure_3)
65 |
66 |
67 |
68 | ## Java 部分(四)线程、多线程和线程池
69 |
70 | 1. [开启线程的三种方式?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_1)
71 | 2. [线程和进程的区别?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_2)
72 | 3. [为什么要有线程,而不是仅仅用进程?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_3)
73 | 4. [run() 和 start() 方法的区别?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_4)
74 | 5. [如何控制某个方法允许并发访问线程的个数?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_5)
75 | 6. [在 Java 中 wait() 和 sleep() 方法的区别](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_6)
76 | 7. [谈谈 wait/notify 关键字的理解](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_7)
77 | 8. [什么导致线程阻塞?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_8)
78 | 9. [线程如何关闭?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_9)
79 | 10. [讲一下 Java 中的同步的方法](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_10)
80 | 11. [数据一致性如何保证?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_11)
81 | 12. [如何保证线程安全?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_12)
82 | 13. [如何实现线程同步?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_13)
83 | 14. [两个进程同时要求读写,能不能实现?如何防止进程同步?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_14)
84 | 15. [线程间操作 List](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_15)
85 | 16. [Java 中对象的生命周期](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_16)
86 | 17. [说说线程池](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_17)
87 | 18. [并发编程面临的挑战有哪些?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#java_thread_18)
88 | 19.
89 |
90 |
91 |
92 | ## Android 部分(一)基础知识点
93 |
94 | 1. [四大组件是什么?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_1)
95 | 2. [四大组件的生命周期和简单用法](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_2)
96 | 3. [Context的理解?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_3)
97 | 4. [AsyncTask详解](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_4)
98 | 5. [Android虚拟机以及编译过程](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_5)
99 | 6. [进程保活方案](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_6)
100 | 7. [Android 消息机制](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_7)
101 | 8. [Window、Activity、DecorView以及ViewRoot之间的关系](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_8)
102 | 9. [Android事件分发机制](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_9)
103 | 10. [dp、sp、px的理解以及相互转换](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_10)
104 | 11. [RelativeLayout和LinearLayout在实现效果同等的情况下使用哪个?为什么?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_11)
105 | 12. [布局相关的 \、\ 控件作用及实现原理](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_12)
106 | 13. [Fragment详解](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_13)
107 | 14. [Json、XML](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_14)
108 | 15. [Assets目录与res目录的区别](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_15)
109 | 16. [View视图绘制过程原理](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_16)
110 | 17. [解决滑动冲突的方式?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_17)
111 | 18. [APP Build过程](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_18)
112 | 19. [Android利用scheme协议进行跳转](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_19)
113 | 20. [MVC、MVP](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_20)
114 | 21. [SurfaceView](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_21)
115 | 22. [HandlerThread](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_22)
116 | 23. [IntentService](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_23)
117 | 24. [谈谈你对Application类的理解](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_24)
118 | 25. [Android为什么要设计出Bundle而不是直接使用HashMap来进行数据传递?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_25)
119 | 26. [SharedPreference在使用过程中有什么注意点?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_26)
120 | 27. [SQLite有哪些可以优化的地方?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_27)
121 | 28. [嵌滑滑动机制](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_28)
122 | 29. [RecyclerView 优化](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_base_29)
123 |
124 |
125 |
126 | ## Android 部分(二)高级知识点
127 |
128 | 1. [Android 系统架构](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_1)
129 | 2. [Android 各个版本特性](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_2)
130 | 3. [Android 系统启动流程](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_3)
131 | 4. [应用程序进程启动过程](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_4)
132 | 5. [Activity 状态的保存与恢复](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_5)
133 | 6. [Android 动画框架实现原理](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_6)
134 | 7. [requestLayout、onLayout、onDraw、drawChild 区别与联系](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_7)
135 | 8. [requestLayout、invalidate、postInvalidate 的区别](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_8)
136 | 9. [Activity、Window、View 的区别及联系](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_9)
137 | 10. [Volley 的理解](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_10)
138 | 11. [如何优化自定义 View](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_11)
139 | 12. [低版本如何实现高版本 API](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_12)
140 | 13. [描述一次网络请求的过程](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_13)
141 | 14. [HttpUrlConnection 与 OkHttp 的关系](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_14)
142 | 15. [Bitmap 的理解](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_15)
143 | 16. [Looper 架构](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_16)
144 | 17. [ActivityThread 的工作原理](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_17)
145 | 18. [AMS 的工作原理](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_18)
146 | 19. [WMS 的工作原理](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_19)
147 | 20. [自定义 View 如何考虑机型适配](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_20)
148 | 21. [自定义 View 的事件](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_21)
149 | 22. [LaunchMode 应用场景](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_22)
150 | 23. [SpareArray 原理](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_23)
151 | 24. [ContentProvider 是如何实现数据共享的](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_24)
152 | 25. [Service 与 Activity 的通信方式](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_25)
153 | 26. [IntentService 原理与作用](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_26)
154 | 27. [ApplicationContext 与 ActivityContext 的区别](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_27)
155 | 28. [SP 是进程同步的嘛?如何做到进程同步?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_28)
156 | 29. [谈谈多线程在 Android 中的使用](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_29)
157 | 30. [进程和 Application 的生命周期](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_30)
158 | 31. [封装 View 时候这么知道 View 的大小?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_31)
159 | 32. [RecycleView 原理](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_32)
160 | 33. [AndroidManifest 的作用和理解](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_33)
161 | 34. [Touch 事件流程](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_34)
162 | 35. [时间分发中的 onTouch 和 onTouchEvent 有什么区别?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_35)
163 | 36. [View 和 ViewGroup 分别有哪些与事件分发有关的回掉方法](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_36)
164 | 37. [View 的刷新机制](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_37)
165 | 38. [View 的绘制流程](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_38)
166 | 39. [自定义控件原理?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_39)
167 | 40. [为什么不能在子线程更新 UI?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_40)
168 | 41. [ANR 产生原因是什么?如何定位和修正?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_41)
169 | 42. [什么情况下会导致 OOM,如何避免?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_42)
170 | 43. [OOM 是否可以 try-catch](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_43)
171 | 44. [内存泄露发生的情况?如何避免](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_44)
172 | 45. [LruCache 默认缓存大小?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_45)
173 | 46. [ContentProvider 的权限管理](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_46)
174 | 47. [如何通过广播拦截一条短信](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_47)
175 | 48. [广播是否可以请求网络?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_48)
176 | 49. [计算一个 View 的嵌套层级?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_49)
177 | 50. [Activity 栈](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_50)
178 | 51. [Android 线程有没有限制?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_51)
179 | 52. [线程池有没有上限?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_52)
180 | 53. [有没有尝试简化 Parcelable 的使用?](https://github.com/Omooo/Android_QA/blob/master/Answer.md#android_advance_53)
181 |
182 | ## 数据结构
183 |
184 | 1. [线性表]()
185 |
186 |
187 |
188 | ## 计算机网络
189 |
190 | 1. [TCP 和 UDP 的区别](https://github.com/Omooo/Android_QA/blob/master/Answer.md#net_1)
191 | 2. [Http1.1和Http1.0及2.0的区别](https://github.com/Omooo/Android_QA/blob/master/Answer.md#net_2)
192 | 3. [DNS解析过程](https://github.com/Omooo/Android_QA/blob/master/Answer.md#net_3)
193 | 4. [HTTP请求报文和响应报文](https://github.com/Omooo/Android_QA/blob/master/Answer.md#net_4)
194 | 5. [简述HTTP](https://github.com/Omooo/Android_QA/blob/master/Answer.md#net_5)
195 | 6.
196 |
--------------------------------------------------------------------------------
/操作系统/中断与系统调用.md:
--------------------------------------------------------------------------------
1 | ---
2 | 中断与系统调用的区分?
3 | ---
4 |
5 | #### 典型回答
6 |
7 | 能够让设备通知 CPU 的硬件机制称为中断。
8 |
9 | 基本中断机制的工作原理如下。CPU 硬件有一条线,称作中断请求线(IRL);CPU 在执行完每条指令后,都会检查 IRL。当 CPU 检测到控制器已在 IRL 上发出一个信号时,CPU 执行状态保存并且跳到内存固定位置的中断处理程序。中断处理程序确定中断原因,执行必要处理,执行状态恢复,并且执行返回中断指令以便 CPU 回到中断前的执行状态。我们说,设备控制器通过中断请求线发送信号引起中断,CPU 捕获中断并且分派到中断处理程序,中断处理程序通过处理设备来清除中断。
10 |
11 | 操作系统提供大量服务。在最低层,系统调用允许运行程序直接向操作系统发出请求。在高层,命令解释程序或外壳提供用户不必编写程序就能发出请求的机制。
12 |
13 | 系统调用提供操作系统服务接口。系统调用大致可以分为六大类:进程控制、文件管理、设备管理、信息维护、通信和保护。
14 |
15 | 简单来说,中断是设备通知 CPU 的一种机制,比如 I/O 执行完成需要通知 CPU 可以开始处理了。系统调用是程序员写的一些程序,最终要通过系统调用去实施,比如我们读写文件时,其实底层都是操作系统通过系统调用去做的,也就是从用户态到内核态。
16 |
17 |
--------------------------------------------------------------------------------
/操作系统/什么是死锁,死锁产生的条件以及处理方法.md:
--------------------------------------------------------------------------------
1 | ---
2 | 什么是死锁,死锁产生的条件以及处理方法?
3 | ---
4 |
5 | #### 典型回答
6 |
7 | 当一个进程申请资源时,如果这时没有可用资源,那么这个进程就进入等待状态。有时,如果所申请的资源被其他等待进程占用,那么该等待进程有可能再也无法改变状态,这种情况称为死锁。
8 |
9 | 如果在一个系统中以下四个条件同时成立,那么就能引起死锁:
10 |
11 | * 互斥:至少有一个资源必须处于非共享模式,即一次只有一个进程可使用。如果另一个进程申请该资源,那么申请进程应等到该资源释放为止。
12 | * 占用并等待:一个进程应占有至少一个资源,并等待另一个资源,而该资源为其他进程所占有。
13 | * 非抢占:资源不能被抢占,即资源只能被进程在完成任务后自愿释放。
14 | * 循环等待:有一组等待进程 {P0,P1,· · · Pn},P0 等待的资源为 P1 占有,P1 等待的资源为 P2 占有,Pn-1 等待的资源为 Pn 占有,Pn 等待的资源为 P0 占有。
15 |
16 | 我们强调所有四个条件必须同时成立才会出现死锁。循环等待条件意味着占有并等待条件,这样说四个条件其实并不是完全独立的。然鹅,后面我们会看到分开考虑这些条件还是有用的。
17 |
18 | 死锁处理方法一般来说有三种:
19 |
20 | 1. 通过协议来预防或避免死锁,确保系统不会进入死锁状态
21 | 2. 可以允许系统进入死锁状态,然后检测它,并加以恢复
22 | 3. 忽视这个问题,认为死锁不可能在系统内发生
23 |
24 | 第三种解决方案为大多数系统所采用,包括 Linux 和 Windows。
25 |
26 | #### 知识延伸
27 |
28 | 下面着重讲解死锁的处理方法。
29 |
30 | ##### 死锁预防
31 |
32 | 发生死锁有四个必要条件,只要确保至少一个必要条件不成立,就能预防死锁。
33 |
34 | * 互斥
35 |
36 | 互斥条件必须成立。可共享资源不要求互斥访问,因此不会参与死锁。只读文件是一个很好的共享资源例子。然鹅,通常不能通过否定互斥条件来预防死锁,因为有的资源本身就是非共享的。例如,一个互斥锁不能同时被多个进程所共享。
37 |
38 | * 持有并等待
39 |
40 | 为了确保持有并等待条件不会出现在系统中,应保证:当每个进程申请一个资源时,它不能占有其他资源。一种可以采用的协议是,每个进程在执行前申请并获得所有资源。另外一种协议允许进程仅在没有资源时才可申请资源。一个进程可申请一些资源并使用它们。然而,在它申请更多其他资源之前,它应释放现已分配的所有资源。
41 |
42 | 这两种协议有两个主要缺点。第一,资源利用率可能比较低,因为许多资源可能已分配,但是很长时间没有被使用。第二,可能发生饥饿。
43 |
44 | * 无抢占
45 |
46 | 为了确保这一条件不成立,可以采用如下协议:如果一个进程持有资源并申请另一个不能立即分配的资源,那么它现在持有的资源都可被抢占。换句话说,这些资源都被隐式释放了。
47 |
48 | 这个协议通常用于状态可以保存和恢复的资源,如 CPU 寄存器和内存。它一般不适用于其他资源,如互斥锁和信号量。
49 |
50 | * 循环等待
51 |
52 | 确保这个条件不成立的一个方法是:对所有资源类型进行完全排序,而且要求每个进程按递增顺序来申请资源。
53 |
54 | **死锁避免**
55 |
56 | 在前面讨论死锁预防算法中,通过限制如何申请资源来预防死锁。这种限制确保,至少有一个死锁的必要条件不会发生。然而,通过这种方法预防死锁有副作用:设备使用率低和系统吞吐量低。
57 |
58 | 避免死锁的另一种方法需要额外信息,即如何申请资源。每个进程都应声明可能需要的每种类型资源的最大数量。鉴于这个先验信息,有可能构造一个算法,以便确保系统不会进入死锁状态。死锁避免算法动态检查资源分配状态,以便确保循环等待条件不能成立。资源分配状态包括可用的资源、已分配的资源及进程的最大需求。
59 |
60 | 下面讨论两个死锁避免算法:
61 |
62 | 资源分配图算法:
63 |
64 | 如果有一个资源分配系统,它的每种资源类型只有一个实例。在资源分配图中,如果没有环存在,那么资源的分配就使得系统处于安全状态,如果有环存在,那么分配会导致系统处于非安全状态。
65 |
66 | 银行家算法:
67 |
68 | 对于每种资源类型有多个实例的资源分配系统,资源分配图算法就不适用了。
69 |
70 | 当一个新的进程进入系统时,它应声明可能需要的每种资源类型实例的最大数量,这一数量不能超过系统资源的总和。当用户申请一组资源时,系统应确定这些资源的分配是否仍然会使系统处于安全状态。如果会,就分配资源;否则,进程应等待,直到某个进程释放足够多资源为止。
71 |
72 | **死锁检测与恢复**
73 |
74 | 如果所有资源类型只有单个实例,我们可以定义这样一个死锁检测算法,该算法使用了资源分配图的一个变形,称为等待图。当每种资源类型有多个实例时,可以使用类似银行家算法。
75 |
76 | 当检测算法确定已有死锁时,打破死锁有两个选择。一个是,简单地中止一个或多个进程来打破循环等待。另一个是,从一个或多个死锁进程那里抢占一个或多个资源。
77 |
78 | **忽视死锁**
79 |
80 | 当发生死锁时,会导致系统性能下降,因为资源被不能运行的进程占有,而越来越多的进程会因申请资源而进入死锁。最后整个系统会停止工作,且需要人工重新启动。
81 |
82 | 虽然这看起来似乎不是一个解决死锁问题的可行方法,但是它却为大多数操作系统所采用。代价是一个重要的考虑因素。忽略死锁的可能性要比其他方法更便宜。对于许多系统,死锁很少发生(如一年一次),因为,与使用频繁的并且开销昂贵的死锁预防、死锁避免和死锁检测与恢复相比,这种方法更为便宜。
--------------------------------------------------------------------------------
/操作系统/什么是虚拟内存.md:
--------------------------------------------------------------------------------
1 | ---
2 | 什么是虚拟内存?
3 | ---
4 |
5 | #### 典型回答
6 |
7 | 我们期望能够执行逻辑地址空间大于可用物理地址空间的进程。虚拟内存是一种技术,能够将较大的逻辑地址空间映射到较小的物理内存。虚拟内存允许采用允许运行极大的进程,提高多道程度,提高 CPU 利用率。再者,它使程序员不必担心内存可用性。此外,虚拟内存允许多个进程共享系统库和内存。通过虚拟内存,可以采用写入时复制进行高效的进程创建,这里的父进程和子进程共享实际的内存页面。
8 |
9 | 虚拟内存的实现通过采用请求调页。纯请求调页只有在页面被引用时才会掉入。首次引用会导致操作系统发生缺页错误。操作系统内核查询内部表,以便确定这个页面在后备存储上的位置。然后,它找到空闲帧,并从后备存储中读取页面;更新页表以反映此更改,并重新启动导致缺页错误的指令。这种方法允许进程运行,即使它的整个内存映像不能同时在内存中。只要缺页错误率足够低,那么性能就可接受。
10 |
11 | #### 知识延伸
12 |
13 | 虚拟内存技术允许执行进程不必完全处于内存。这种方案的一个主要优点就是,程序可以大于物理内存。此外,虚拟内存将内存抽象成一个巨大的、统一的存储数组,进而实现了用户看到的逻辑内存与物理内存的分离。这种技术使得程序员不再担忧内存容量的限制。虚拟内存还允许进程轻松共享文件和实现共享内存。此外,它为创建进程提供了有效的机制。然而,虚拟内存的实现并不容易,并且使用不当还可能会大大降低性能。下面会以请求调页为例来讨论虚拟内存。
14 |
15 | **请求调页**
16 |
17 | 下面就要讨论如何采用请求调页来实现虚拟内存。
18 |
19 | 想一想,如何从磁盘加载可执行程序到内存?
20 |
21 | 一种选择是,在程序执行时将整个程序加载到物理内存。然而,这个方法的一个问题是,最初可能不需要整个程序都处于内存。假设程序开始时带有一组用户可选的选项。加载整个程序会导致所有选项的执行代码都加载到内存中,而不管这些选项是否最终使用。另一种策略是,仅在需要时才加载页面。这种技术被称为请求调页,常常用于虚拟内存系统。对于请求调页的虚拟内存,页面只有在程序执行期间被请求时才被加载。因此,从未访问的那些页从不加载到物理内存。
22 |
23 | 当换入进程时,调页程序会猜测在该进程被再次换出之前会用到哪些页。调页程序不是调入整个进程,而是把那些要使用的页调入内存。这样,调页程序就避免了读入那些不适用的页,也减少了交换时间和所需的物理内存空间。
24 |
25 | 使用这种方案需要一定形式的硬件支持,以区分内存的页面和磁盘的页面。
26 |
27 | 如果进程试图访问那些尚未调入内存中的页面,情况会如何呢?对标记为无效的页面访问会产生缺页错误。分页硬件在通过页表转换地址时会注意到无效位被设置,从而陷入操作系统。这种陷阱是由于操作系统未能将所需的页面调入内存引起的,处理这种缺页错误的程序很简单:
28 |
29 | 1. 检查这个进程的内部表,以确定该引用是有效的还是无效的内存访问
30 | 2. 如果引用无效,那么终止进程。如果引用有效但尚未调入页面,那么现在就应调入
31 | 3. 调度一个空闲帧
32 | 4. 调度一个磁盘操作,以将所需页面读到刚分配的空闲帧
33 | 5. 当磁盘读取完成时,修改进程的内部表和页表,以指示该页现在处于内存中
34 | 6. 重新启动被陷阱中断的指令。该进程现在能访问所需的页面,就好像它总是在内存中
35 |
36 | 请求调页的关键要求是在缺页错误后重新启动任何指令的能力。
37 |
38 | 页面置换是请求调页的基础。它完成了逻辑内存和物理内存之间的分离,采用这种机制,较小的物理内存能为程序员提供巨大的虚拟内存。
39 |
40 | 为实现请求调页,必须解决两个主要问题:应设计帧分配算法和页面置换算法。也就是说,如果有多个进程在内存中,则必须决定要为每个进程分配多少帧;并且当需要页面置换时,必须选择要置换的帧。设计适当的算法来解决这些问题是个重要任务。
41 |
42 | 通过请求调页可以减少分配给进程的帧数。这种安排可以增加多道程度,并且增加系统 CPU 的利用率。即使进程内存需要超过了总的物理内存,也允许执行。这些进程运行在虚拟内存中。
43 |
44 | 如果总的内存需求超过物理内存的容量,则可能需要置换内存的页面,以生产新页面的空闲帧。可以使用多种页面置换算法。FIFO 页面置换算法容易编程,但是会遭受 Belady 异常。最优页面置换需要前置信息。LRU 页面置换是最优页面置换的近似,但是仍难以实现。大多数页面置换算法,例如第二次机会算法,是 LRU 置换的近似。
45 |
46 | 除了页面置换算法之外,还需要帧分配策略。分配可以是固定的,此时建议采用局部页面置换算法;也可以是动态的,此时建议采用全局置换。工作集模型假定进程执行的局部性。工作集是当前局部的所有页面的集合。因此,应当为每个进程的当前工作集分配足够多的帧。如果一个进程没有足够的内存用于工作集,则会发生抖动。为进程提供足够的内存以避免抖动,可能需要进程交换和调度。
47 |
48 | 大多数操作系统提供内存映射文件的能力,如允许文件 I/O 作为常规内存访问。Win32 API 通过文件的内存映射来实现共享内存。
49 |
50 | 内核进程通常需要采用物理连续页面来分配内存。伙伴系统允许内核进程按 2 的幂大小来分配,这通常会导致碎片。slab 分配器允许从由 slab 组成的 cache 上分配,每个 slab 由若干物理连续的页面组成。采用 slab 分配,不会因碎片问题而产生内存浪费,并且内存请求可以快速满足。
--------------------------------------------------------------------------------
/操作系统/内存管理中分页和分段有什么区别?.md:
--------------------------------------------------------------------------------
1 | ---
2 | 内存管理中分页和分段有什么区别?
3 | ---
4 |
5 | #### 典型回答
6 |
7 | 分段就是支持用户视图的内存管理方案。逻辑地址空间是由一组段构成。每个段都有名称和长度。地址指定了段名称和段内偏移。因此用户通过两个量来指定地址:段名称和段偏移。
8 |
9 | 实现分页需要操作系统和计算机硬件的协作。
10 |
11 | 实现分页的基本方法涉及将物理内存分为固定大小的块,称为页帧;而将逻辑内存也分为同样大小的块,称为页面。逻辑地址空间现在完全独立于物理地址空间。
12 |
13 | 分段允许进程的物理地址空间是非连续的。分页是提供这种优势的另一种内存管理方案。然鹅,分页避免了外部碎片和紧缩,而分段不可以。分页也避免了将不同大小的内存块匹配到交换空间的麻烦问题。
14 |
15 | #### 知识延伸
16 |
17 | 首先先来熟悉一下逻辑地址空间和物理地址空间。
18 |
19 | CPU 生成的地址通常称为逻辑地址,而内存单元看到的地址(即加载到内存地址寄存器)通常称为物理地址。
20 |
21 | 编译时和加载时的地址绑定方法生成相同的逻辑地址和物理地址。然而,执行时的地址绑定方案生成不同的逻辑地址和物理地址。在这种情况下,我们通常称逻辑地址为虚拟地址。通常,我们并不区分逻辑地址和虚拟地址。由程序所生成的所有逻辑地址的集合称为逻辑地址空间,这些逻辑地址对应的所有的物理地址的集合称为物理地址空间。因此,对于执行时地址绑定方案,逻辑地址空间和物理地址空间是不同的。
22 |
23 | 从虚拟地址到物理地址的运行时映射是由内存管理单元(MMU)的硬件设备来完成的。用户程序不会看到真实的物理地址,用户程序处理逻辑地址;内存映射硬件将逻辑地址转变为物理地址。
24 |
25 | **连续内存分配**
26 |
27 | 内存通常分为两个区域:一个用于驻留操作系统,一个用户用户进程。最简单的内存分配方法之一,就是将内存分为多个固定大小的分区。每个分区可以只包含一个进程。
28 |
29 | 对于可变分区方案,操作系统有一个表,用于记录哪些内存可用和哪些内存已用。开始,所有内存都可用于用户进程,因此可以作为一大块的可用内存,称为孔。最后,正如将会看到的,内存有一个集合,以包含各种大小的孔。
30 |
31 | 综上所述,可用的内存块为分散在内存里的不同大小的孔的集合。当新进程需要内存时,系统为该进程查找足够大的孔。如果孔太大,那么就分为两块:一块分配给新进程,另一块还回到孔集合。
32 |
33 | 从一组可用孔中选择一个空闲孔的最为常用方法包括:首次适应、最优适应及最差适应。
34 |
35 | 首次适应和最优适应都有外部碎片的问题,外部碎片的一个可能解决方案是紧缩。它的目的是移动内存内容,以便将所有空闲空间合并成一整块。然而,紧缩并非总是可能的。如果重定位是静态的,并且在汇编时或加载时进行的,那么就不能紧缩。只有重定位是动态的,并且在运行时进行的,才可用紧缩。
36 |
37 | 外部碎片化问题的另一个可能的解决方案是:允许进程的逻辑地址空间是不连续的;这样,只要有物理内存可用,就允许为进程分配内存。有两种互补的技术可以实现这个解决方案:分段和分页。
38 |
39 | ##### 分段
40 |
41 | 分段的每个段都有名称和长度,地址制定了段名称和段内偏移。
42 |
43 | 虽然用户现在能够通过二维地址来引用程序内的对象,但是实际物理内存仍然是一维的字节序列。因此,我们应该定义一个实现方式,以便映射用户定义的二维地址到一维物理地址。这个地址是通过段表来实现的。段表的每个条目都有段基地址和段界限。段基地址包含该段在内存中的开始物理地址,而段界限指定该段的长度。
44 |
45 | 段表的使用如下图所示:
46 |
47 | 
48 |
49 | 每个逻辑地址由两部分组成:段号 s 和段偏移 d。段号用作段表的索引,逻辑地址的偏移 d 应位于 0 和段界限之间。如果不是这样,那么会陷入操作系统中(逻辑地址试图访问段的外面)。如果偏移 d 合法,那么就与基地址相加而得到所需字节的物理内存地址。因此,段表实际上基址寄存器值和界限寄存器的值的对的数组。
50 |
51 | 作为一个例子,可以看下图:
52 |
53 | 
54 |
55 | **分页**
56 |
57 | 实现分页的基本方法涉及将物理内存分为固定大小的块,称为页帧;而将逻辑内存也分为同样大小的块,称为页面。
58 |
59 | 分页的硬件支持如下图所示:
60 |
61 | 
62 |
63 | 由 CPU 生成的每个地址分为两个部分:页码(p)和页偏移(d)。页码作为页表的索引。页表包含每页所在物理内存的基地址,这个基地址与页偏移的组合就形成了物理内存地址,可发送到物理单元。
64 |
65 | 内存分页模型如下图:
66 |
67 | 
68 |
69 | 页大小是由硬件来决定的。页的大小为 2 的幂;根据计算机体系结构的不同,页大小可从 512 字节到 1 GB 不等。
70 |
71 | 采用分页方案不会产生外部碎片:每个空闲帧都可以分配给需要它的进程。不过,分页有内部碎片。注意,分配是以帧为单位进行的。如果进程所要求的内存并不是页的整数倍,那么最后一个页可能用不完。
72 |
73 | 现在,页大小通常为 4~8 KB。
74 |
75 | 分页的一个重要方面是,程序员视图的内存和实际的物理内存的清楚分离。程序员将内存作为一整块来处理,而且它只包含这一个程序。事实上,一个用户程序与其他程序一起,分散在物理内存上。程序员视图的内存和实际的物理内存的不同是通过地址转换硬件来协调的。逻辑地址转变成物理地址。这种映射,程序员是不知道的,它是由操作系统控制的。注意,根据定义,用户进程不能访问不属于它的内存。
76 |
77 | 由于操作系统管理物理内存,它应知道物理内存的分配细节:哪些帧已分配,哪些帧空着,总共有多少帧,等等。这些信息通常保存在称为帧表的数据结构中。在帧表中,每个条目对应着一个帧,以表示该帧是空闲还是已占用;如果占用,是被哪个进程所占用。
78 |
79 | 每个操作系统都有自己保存页表的方法,有的为每个进程分配一个页面。页表的指针,与其他寄存器的值(如指令寄存器),一起存入进程控制块。
80 |
81 | 组织页表有分层分页、哈希页表和倒置页表。
82 |
83 | #### 小结
84 |
85 | 多道程序操作系统的内存管理算法包括从简单的单用户系统方案,到分段和分页的方案。决定特定系统采用方案的最重要因素是所提供的硬件支持。CPU 所产生的每个内存地址都应先进行合法性检查,然后才可能映射到物理地址。检查不能通过软件来实现,因此受到可用硬件的限制。
86 |
87 | 不同的内存管理算法(连续分配、分页、分段及分段分页组合)在许多方面都不同。当比较不同内存管理策略时,需要考虑如下几点:
88 |
89 | * 硬件支持
90 |
91 | 对单分区和多分区方案,只需要一个基地址寄存器或一个基地址 - 界限地址寄存器对就足够了;而对于分页和分段,需要映射表来定义地址映射。
92 |
93 | * 性能
94 |
95 | 随着内存管理算法变得更为复杂,逻辑地址到物理地址的映射时间也会更长。对于简单的系统,只需对逻辑地址进行比较和加法操作。如果映射表通过快速寄存器实现,那么分页和分段操作也会很快。然而,如果映射表位于内存中,那么用户内存访问就大受影响。TLB 可使性能影响减小到可接受的水平。
96 |
97 | * 碎片
98 |
99 | 如果多道程序的程度更高,那么多道程序系统的执行通常会更有效。给定一组进程,通过加载更多进程到内存,可以增加多道程序的程度。为了完成这个任务,应降低内存浪费或碎片。采用固定大小分配单元(如单个分区和分页)的系统会有内部碎片的问题。采用可变大小分配单元(如多个分区和分段)的系统会有外部碎片问题。
100 |
101 | * 重定位
102 |
103 | 外部碎片问题的解决方案之一是紧缩。紧缩就是在内存中移动程序,且不会影响程序的运行。这种方案要求,执行时的逻辑地址可动态的进行重定位。如果只能进行加载时的地址重定位,那么就不能采用紧缩。
104 |
105 | * 保护
106 |
107 | 如果提供了分页或分段,那么用户程序的不同区域可以声明为只可执行的、只读的或可读可写的。对于代码和数据的共享,这种限制是必要的;对于程序设计的常见错误,能提供运行时的简单检查。
--------------------------------------------------------------------------------
/操作系统/操作系统面试题集.md:
--------------------------------------------------------------------------------
1 | ---
2 | 操作系统
3 | ---
4 |
5 | 1. [进程和线程的区别?](https://github.com/Omooo/Android_QA/blob/master/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E8%BF%9B%E7%A8%8B%E5%92%8C%E7%BA%BF%E7%A8%8B%E7%9A%84%E5%8C%BA%E5%88%AB.md)
6 | 2. [进程有哪几种状态?](https://github.com/Omooo/Android_QA/blob/master/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E8%BF%9B%E7%A8%8B%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%E7%8A%B6%E6%80%81.md)
7 | 3. [进程间通信的几种方式](https://github.com/Omooo/Android_QA/blob/master/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E7%9A%84%E5%87%A0%E7%A7%8D%E6%96%B9%E5%BC%8F.md)
8 | 4. [进程调度策略](https://github.com/Omooo/Android_QA/blob/master/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E8%BF%9B%E7%A8%8B%E8%B0%83%E5%BA%A6%E7%AD%96%E7%95%A5.md)
9 | 5. 进程同步的几种机制
10 | 6. 线程有哪几种状态?
11 | 7. 线程同步的方式
12 | 8. [什么是死锁,死锁产生的条件以及处理方法](https://github.com/Omooo/Android_QA/blob/master/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E4%BB%80%E4%B9%88%E6%98%AF%E6%AD%BB%E9%94%81%EF%BC%8C%E6%AD%BB%E9%94%81%E4%BA%A7%E7%94%9F%E7%9A%84%E6%9D%A1%E4%BB%B6%E4%BB%A5%E5%8F%8A%E5%A4%84%E7%90%86%E6%96%B9%E6%B3%95.md)
13 | 9. [内存管理中分页和分段有什么区别?](https://github.com/Omooo/Android_QA/blob/master/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E4%B8%AD%E5%88%86%E9%A1%B5%E5%92%8C%E5%88%86%E6%AE%B5%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F.md)
14 | 10. [什么是虚拟内存?](https://github.com/Omooo/Android_QA/blob/master/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E4%BB%80%E4%B9%88%E6%98%AF%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98.md)
15 | 11. 什么是缓冲区溢出?
16 | 12. 中断与系统调用的区别?
17 |
18 |
--------------------------------------------------------------------------------
/操作系统/缓冲区溢出.md:
--------------------------------------------------------------------------------
1 | ---
2 | 缓冲区溢出
3 | ---
4 |
5 | #### 典型回答
6 |
7 | 缓冲区溢出是针对程序设计缺陷,向程序输入缓冲区写入使之溢出的内容(通常是超过缓冲区能保存的最大数据量的数据),从而破坏程序运行、趁著中断之际并获取程序乃至系统的控制权。
8 |
9 | 缓冲区溢出原指当某个数据超过了处理程序限制的范围时,程序出现的异常操作。造成此现象的原因有:
10 |
11 | * 存在缺陷的程序设计
12 | * 尤其是 C 语言,不像其他一些高级语言会自动进行数组或指针的边界检查,增加溢出风险
13 | * C 语言中的 C 标准库还具有一些非常危险的操作函数,使用不当也为溢出创造条件
14 |
15 | #### 参考资料
16 |
17 | [维基百科_缓冲区溢出](https://zh.wikipedia.org/wiki/%E7%BC%93%E5%86%B2%E5%8C%BA%E6%BA%A2%E5%87%BA)
--------------------------------------------------------------------------------
/操作系统/进程同步的几种机制.md:
--------------------------------------------------------------------------------
1 | ---
2 | 进程同步的几种机制
3 | ---
4 |
5 |
--------------------------------------------------------------------------------
/操作系统/进程和线程的区别.md:
--------------------------------------------------------------------------------
1 | ---
2 | 进程和线程的区别?
3 | ---
4 |
5 | #### 典型回答
6 |
7 | 1. 进程是操作系统资源分配的基本单位,而线程是 CPU 调度和分配的基本单位
8 | 2. 进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其他进程产生影响,而线程只是一个进程中的不同的执行路径
9 | 3. 进程是执行的程序,线程是进程内的控制流。
10 |
11 | #### 知识延伸
12 |
13 | 进程是现代分时操作系统的工作单元,进程是执行的程序,这是一种非正式的说法。进程不只是程序代码,进程还包括当前活动,如程序计数器、进程堆栈和数据段。
14 |
15 | 操作系统内的每个进程表示,采用进程控制块(Process Control Block,PCB),它包含了许多与某个特定进程相关的信息:
16 |
17 | * 进程状态:状态可以包括新的、就绪、运行、等待、停止等。
18 | * 程序计数器:计数器表示进程将要执行的下个指令的地址。
19 | * CPU 寄存器:根据计算机体系结构的不同,寄存器的类型和数量也会不同。它们包括累加器、索引寄存器、堆栈指针、通用寄存器和其他条件码信息寄存器。在发生中断时,这些状态信息与程序计数器一起需要保存,以便进程以后能正确的继续执行。
20 | * CPU 调度信息:这类信息包括进程优先级、调度队列的指针和其他调度参数。
21 | * 内存管理信息:根据操作系统使用的内存系统,这类信息可以包括基地址和界限寄存器的值、页表或段表。
22 | * 记账信息:这类信息包括 CPU 时间、实际使用时间、时间期限、记账数据、作业或进程数量等。
23 | * I/O 状态信息:这类信息包括分配给进程的 I/O 设备列表、打开文件列表等。
24 |
25 | PCB 结构如下图:
26 |
27 | 
28 |
29 | 每个线程是 CPU 使用的一个基本单元;它包括线程 ID、程序计数器、寄存器组和堆栈。。它与同一进程的其他线程共享代码段、数据段和其他操作系统资源,如打开文件和信号。每个传统或重量级进程只有单个控制线程,如果一个进程具有多个控制线程,那么它能同时执行多个任务,下图说明了传统单线程进程和多线程进程的差异:
30 |
31 | 
32 |
33 | 对于多处理器体系结构,多线程的优点更大,因为线程可以在多处理核上并行运行。不管有多少可用 CPU,单线程进程只能运行在一个 CPU 上。
34 |
35 | #### 扩展阅读
36 |
37 | 知乎上有一个对于 “线程和进程的区别” 的回答非常好,总结一句话是:
38 |
39 | **进程和线程都是一个时间段的描述,是 CPU 工作时间段的描述,只是颗粒不同罢了。**
40 |
41 | 原文地址如下:
42 |
43 | [https://www.zhihu.com/question/25532384/answer/81152571](https://www.zhihu.com/question/25532384/answer/81152571)
44 |
--------------------------------------------------------------------------------
/操作系统/进程有哪几种状态.md:
--------------------------------------------------------------------------------
1 | ---
2 | 进程有哪几种状态?
3 | ---
4 |
5 | #### 典型回答
6 |
7 | 进程在执行时会改变状态。进程状态,部分取决于进程的当前活动。每个进程可能处于以下状态:
8 |
9 | * 新的(New):进程正在创建
10 | * 运行(Running):指令正在执行
11 | * 等待(Waiting):进程等待发生某个事件(如 I/O 完成或收到信号)
12 | * 就绪(Ready):进程等待分配处理器
13 | * 终止(Terminated):进程完成执行
14 |
15 | 这些状态名称比较随意,而且随着操作系统的不同而有所不同。不过,它们表示的状态在所有操作系统上都会出现。有的操作系统对进程状态定义的更细。重要的是认识到:一次只有一个进程可以在一个处理器上运行,但是许多进程可处于就绪或等待状态。下面是一个进程状态图:
16 |
17 |
18 |
19 | 
--------------------------------------------------------------------------------
/操作系统/进程调度策略.md:
--------------------------------------------------------------------------------
1 | ---
2 | 进程调度策略
3 | ---
4 |
5 | #### 典型回答
6 |
7 | CPU 调度是多道程序操作系统的基础。通过在进程间切换 CPU,操作系统可以使得计算机更加高效。每当 CPU 空闲时,操作系统就应从就绪队列中选择一个进程来执行。CPU 调度算法有以下几种:
8 |
9 | 1. 先到先服务调度
10 | 2. 最短作业优先调度
11 | 3. 优先级调度
12 | 4. 轮转调度
13 | 5. 多级队列调度
14 | 6. 多级反馈队列调度
15 |
16 | 先来先服务(FCFS)调度是最简单的调度算法,但是它会让短进程等待很长的进程。最短作业优先调度(SJF)调度可证明是最佳的,提供最短的平均等待时间,然而,SJF 的实现是很难的,因为预测下一个 CPU 执行的长度是难的。SJF 算法是通用优先级调度算法(简单分配 CPU 到具有最高优先级的进程)的一个特例。优先级和 SJF 的调度可能产生饥饿。老化技术阻止饥饿。
17 |
18 | 轮转(RR)调度更适合于分时(交互)系统。RR 调度为就绪队列的首个进程,分配 q 个时间单位,这里 q 是时间片。在 q 个时间单位之后,如果该进程还没有释放 CPU,那么它被抢占并添加到就绪队列的尾部。该算法的主要问题是选择时间片,如果时间片太大,那么 RR 调度就成了 FCFS 调度;如果时间片太小,那么由于上下文切换引起的调度开销就过大。
19 |
20 | FCFS 调度算法是非抢占的,而 RR 调度算法是抢占的。SJF 和优先级算法可以是抢占的,也可以是非抢占的。
21 |
22 | 多级队列算法允许多个不同算法用于不同类型的进程。最常用的模型包括:使用 RR 调度的前台交互队列与使用 FCFS 调度的后台批处理队列。多级反馈队列允许进程在队列之间迁移。
23 |
24 | #### 知识延伸
25 |
26 | 对于支持线程的操作系统,操作系统实际调度的是内核级线程而非进程。
27 |
28 | 用户级线程是由线程库来管理的,而内核并不知道它们。用户级线程为了运行在 CPU 上,最终应映射到相关的内核级线程,但是这种映射可能不是直接的,可能采用轻量级进程(LWP)。
29 |
30 | **多处理器调度**
31 |
32 | 对于多处理器系统,CPU 调度的一种方法是让一个处理器处理所有调度决定、I/O 处理以及其他系统活动,其他处理器只执行用户代码。这种非对称多处理很简单,因为只有一个处理器访问系统数据结构,减少了数据共享的需要。
33 |
34 | 第二种方法是使用对称多处理(SMP),即每个处理器自我调度。所有进程可能处于一个共同的就绪队列中,或每个处理器都有它自己的私有就绪进程队列。不管如何,调度这样进行:每个处理器的调度程序都检查共同就绪队列,以便选择执行一个进程。
35 |
36 | 大多数 SMP 系统试图避免将进程从一个处理器移到另一个处理器,而是试图让一个进程运行在同一个处理器上。这称为处理器的亲和性,即一个进程对它运行的处理器具有亲和性。
37 |
38 | 对于 SMP 系统,最重要的是保持所有处理器的负载平衡,以便充分利用多处理器的优点。负载平衡设法将负载平均分配到 SMP 系统的所有处理器。负载平衡通常有两种方法:推迁移和拉迁移。但是,负载平衡往往会抵消掉处理器亲和性的好处,也就是说,保持进程运行在同一个处理器上的好处是进程可以利用它在该处理器缓存内的数据。
39 |
40 | 接下来,我们讨论 Linux 和 Windows 的调度策略。重要的是要注意,我们使用的进程调度这一术语是泛指的。事实上,在讨论 Windows 系统时,采用内核线程调度,而在讨论 Linux 时,采用任务调度。
41 |
42 | **Linux 调度**
43 |
44 | 完全公平调度程序(CFS)是默认的 Linux 调度算法。
45 |
46 | Linux 系统的调度基于调度类,每个类都有一个特定的优先级。内核针对不同的调度类,采用不同的调度算法,以便满足系统与进程的需要。Linux 标准内核实现了两个调度类:采用 CFS 调度算法的默认调度类和实时调度类。
47 |
48 | CFS 调度程序并不采用严格规则来为一个优先级分配某个长度的时间片,而是为每个任务分配一定比例的 CPU 处理时间。
49 |
50 | Linux CFS 调度程序采用高效算法,以便选择运行下个任务。每个可运行的任务放置在红黑树上。
51 |
52 | 当一个任务变得可运行时,它被添加到树上。当一个任务变得不可运行时,它从树上删除。一般来说,得到较少处理时间的任务会偏向树的左侧;得到较多处理时间的任务会偏向树的右侧。由于红黑树是平衡的,找到最左侧结点需要 lg N 的·时间复杂度。从 CFS 调度程序角度而言,这也是具有最高优先级的任务。
53 |
54 | **Windows 调度**
55 |
56 | Windows 采用基于优先级的、抢占调度算法来调度线程。Windows 调度程序确保具有最高优先级的线程总是在运行的。用于处理调度的 Windows 内核部分称为调度程序。
57 |
58 | 调度程序采用 32 级的优先级方案,以便确定线程执行方案。
59 |
60 |
--------------------------------------------------------------------------------
/操作系统/进程间通信的几种方式.md:
--------------------------------------------------------------------------------
1 | ---
2 | 进程间通信的几种方式?
3 | ---
4 |
5 | #### 典型回答
6 |
7 | 1. 套接字
8 |
9 | 套接字为通信的端点。通过网络通信的每对进程需要使用一对套接字,即每个进程各有一个。每个套接字由一个 IP 地址和一个端口号组成。通常,套接字采用 CS 架构,服务器通过监听指定的端口,来等待特定服务。服务器在收到请求后,接受来自客户端套接字的连接,从而完成连接。
10 |
11 | 2. 管道
12 |
13 | 管道提供了一个相对简单的进程间的相互通信,普通管道允许父进程和子进程之间的通信,而命名管道允许不相关进程之间的通信。
14 |
15 | #### 知识延伸
16 |
17 | 进程间通信有两种基本模型:**共享内存**和**消息传递**。
18 |
19 | 共享内存模型会建立起一块供协作进程共享的内存区域,进程通过向此共享区域读出或写入数据来交换信息。消息传递模型通过在协作进程间交换信息来实现通信。
20 |
21 | 下图给出了两个模型的对比:
22 |
23 | 
24 |
25 | 很多系统同时实现了这两种模型。
26 |
27 | 消息传递对于交换较少数量的数据很有用,因为无需避免冲突。对于分布式系统,消息传递也比共享内存更易实现。共享内存可以快于消息传递,这是因为消息传递的实现经常采用系统调用,因此需要更多的时间以便内核介入。与此相反,共享内存系统仅在建立共享内存区域时需要系统调用;一旦建立共享内存,所有访问都可作为常规内存访问,无需借助内核。
28 |
29 | 对具有多个处理核的系统上,消息传递的性能要优于共享内存。共享内存会有高速缓存一致性问题,这是由共享数据在多个高速缓存之间迁移而引起的。随着系统处理核的日益增加,可能导致消息传递作为 IPC 的首选机制。
30 |
31 | ##### 共享内存系统
32 |
33 | 采用共享内存的进程间通信,需要通信进程建立共享内存区域。通常,这一片共享内存区域驻留在创建共享内存段的进程地址空间内。其它希望使用这个共享内存段进行通信的进程应将其附加到自己的地址空间。回忆一下,通常操作系统试图阻止一个进程访问另一个进程的内存。共享内存需要两个或更多的进程同意取消这一限制;这样它们通过在共享区域内读出或写入来交换信息。数据的类型或位置取决于这些进程,而不是受控于操作系统。另外,进程负责确保,它们不向同一位置同时写入数据。
34 |
35 | ##### 消息传递模型
36 |
37 | 消息传递提供一种机制,以便允许进程不必通过共享地址空间来实现通信和同步。对分布式环境(通信进程可能位于通过网络连接的不同计算机),这特别有用。
38 |
39 | 需要通信的进程应有一个方法,以便互相引用。它们可以使用直接或间接的通信。
40 |
41 | 对于直接通信,需要通信的每个进程必须明确指定通信的接受者或发送者。采用这种方案,原语 send() 和 receive() 定义如下:
42 |
43 | ```
44 | send(P, message): 向进程 P 发送 message
45 | receive(Q, message): 从进程 Q 接收 message
46 | ```
47 |
48 | 这种方案展示了寻址的对称性,即发送和接收进程必须指定对方,以便通信。这种方案的一个变形采用寻址的非对称性,即只要发送者指定接受者,而接受者不需要指定发送者。采用这种方案,原语 send() 和 receive() 定义如下:
49 |
50 | ```
51 | send(P, message): 向进程 P 发送 message
52 | receive(id, message): 从任何进程接收 message,这里变量 id 被设置成与其通信进程的名称
53 | ```
54 |
55 | 在间接通信中,通过邮箱或端口来发送和接收消息。邮箱可以抽象成一个对象,进程可以向其中存放消息,也可从中删除消息,每个邮箱都有一个唯一的标识符。一个进程可以通过多个不同邮箱与另一个进程通信,但是两个进程只有拥有一个共享邮箱时才能通信。原语 send() 和 receive() 定义如下:
56 |
57 | ```
58 | send(A, message): 向邮箱 A 发送 message
59 | receive(A, message): 从邮箱 A 接收 message
60 | ```
61 |
62 | #### 客户机 / 服务器通信
63 |
64 | 客户机 / 服务器通信的两种策略:套接字和管道。
65 |
66 | 套接字:
67 |
68 | 套接字为通信的端点。通过网络通信的每对进程需要使用一对套接字,即每个进程各有一个。每个套接字由一个 IP 地址和一个端口号组成。通常,套接字采用 CS 架构,服务器通过监听指定的端口,来等待特定服务。服务器在收到请求后,接受来自客户端套接字的连接,从而完成连接。
69 |
70 | Java 提供三种不同类型的套接字。面向连接的 TCP 套接字用 Socket 类实现。无连接的 UDP 套接字使用 DatagramSocket 类。最后,MulticastSocket 类为 DatagramSocket 类的子类,多播套接字允许数据发送到多个接受者。
71 |
72 | 使用套接字的通信,虽然常用和高效,但是属于分布式进程之间的一种低级形式的通信。一个原因是,套接字只允许在通信线程之间交换无结构的字节流。客户机或服务器需要自己加上数据结构。
73 |
74 | 管道:
75 |
76 | 管道允许两个进程进行通信。管道是早期 UNIX 系统最早使用的一种 IPC 机制。管道为进程之间的相互通信提供了一种较为简单的方法,尽管也有一定的局限性。在实现管道时,应该考虑以下四个问题:
77 |
78 | 1. 管道允许单向通信还是双向通信?
79 | 2. 如果允许双向通信,它是半双工(数据在同一时间内只能按一个方向传输)的还是全双工(数据在同一时间内可在两个方向上传输)的?
80 | 3. 通信进程之间知否有一定的关系(如父子关系)?
81 | 4. 管道通信是否能够通过网络,还是只能在同一机器上进行?
82 |
83 | 下面就讨论两种常见类型的用于 UNIX 和 Windows 系统的管道:普通管道和命名管道。
84 |
85 | **普通管道**
86 |
87 | 普通管道允许两个进程按标准的生产者和消费者方式进行通信:生产者向管道的一端(写入端)写,消费者从管道的另一端(读出端)读。因此,普通管道是单向的,只允许单向通信。如果需要双向通信,那么就要采用两个管道,而每个管道向不同方向发送数据。
88 |
89 | 在 UNIX 系统上,普通管道的创建采用函数:
90 |
91 | ```
92 | pipe(int fd[])
93 | ```
94 |
95 | 这个函数创建了一个管道,以便通过文件描述符 int fd[] 来访问:fd[0] 为管道的读出端,而 fd[1] 为管道的写入端。Unix 将管道作为一种特殊类型的文件。因此,访问管道可以采用普通的系统调用 read() 和 write()。
96 |
97 | 普通管道只能由创建进程所访问。通常情况下,父进程创建了一个管道,并使用它与其子进程通信(该进程由 fork() 来创建)。如前面所述,子进程继承了父进程打开的文件的打开文件。由于管道是一种特殊类型的文件,因此子进程也继承了父进程的管道。
98 |
99 | 对于 Windows 系统,普通管道被称为匿名管道,它们的行为类似于 Unix 的管道:它们是单向的,通信进程之间具有父子关系。
100 |
101 | 采用普通管道的进程需要有父子关系,这意味着,这些管道只可用于同一机器的进程间通信。
102 |
103 | **命名管道**
104 |
105 | 普通管道提供了一个简单的机制,允许一对进程通信。然鹅,只有当进程相互通信时,普通管道才存在。对于 Unix 和 Windows 系统,一旦进程已经完成通信并终止了,那么管道就不存在了。
106 |
107 | 命名管道提供了一个更强大的通信工具。通信可以是双向的,并且父子关系不是必需的。当建立一个命名管道后,多个进程多可用它通信。事实上,在一个典型的场景中,一个管道有多个写者。此外,当通信进程完成后,命名管道继续存在。虽然 Unix 和 Windows 系统都支持命名管道,但是实现细节具有很大不同。
108 |
109 | 对于 Unix,命名管道为 FIFO。一旦创建,它们表现为文件系统的典型文件。通过系统调用 mkfifo(),可以创建 FIFO;通过系统调用 open()、read()、write() 和 close(),可以操作 FIFO。FIFO 会一直存在,直到它被显式的从文件系统中删除。虽然 FIFO 允许双向通信,只允许半双工传输。如果数据要在两个方向上传输,那么通常使用两个 FIFO。此外,通信进程应位于同一台机器上,如果需要两个不同系统之间的通信,那么应该使用套接字。
110 |
111 | 与 UNIX 系统相比,Windows 系统的命名管道通信机制更加丰富。允许全双工通信,并且通信进程可以位于同一机器或不同机器。此外,UNIX 的 FIFO 只支持字节流的数据,而 Windows 系统允许字节流或消息流的数据。
--------------------------------------------------------------------------------
/计算机网络/HTTPS.md:
--------------------------------------------------------------------------------
1 | ---
2 | HTTPS
3 | ---
4 |
5 | 1. HTTPS 的实现原理
6 | 2. HTTPS 的优缺点
7 |
8 |
--------------------------------------------------------------------------------