第1章 课程介绍
基础不牢,地动山摇
拧得好螺丝,才能造飞机。
第2章 不变之法:面试居然就一个套路
注意事项
梳理自身的技能:
- 编程语言功底(Java)
- 你熟悉的语言有哪些亮点特效?
- 你能运用这些特性写什么框架?(ButterKnife)
- android系统原理
- 程序怎么运行的?
- 窗口是怎么显示的?
- 声音是怎么播放的?
- 项目经验
- 有哪些难点如何突破的?
- 有哪些你失败之处?(思考和反思)
- 架构设计
- 为什么选这个方案?(对业内的框架很熟悉,有过对比)
- 如何进行模块边界的划分?(比如jni边界划分)
- 如何实现模块耦合?
了解市场需求:
工程师级别的要求:
- 初级工程师(听话):
- 能做好被安排的一般性工作
- 能判断和解决例行、常规问题
- 中级工程师(能独立解决问题了):
- 能够应用专业知识独立解决较为复杂的问题
- 可在一定指导下独立领导跨部门的项目,能够培训和教导新员工
- 高级工程师(对他的知道已经是很少了):
- 对复杂问题的解决有自己的见解,对问题的识别、优先级分配见解尤其有影响力,善于寻求资源解决问题
- 可独立领导跨部门的项目,能够培训或知道新进员工
- 部门内具专业影响力
明确自己的目标:
- 了解市场需求
- 明确自身水平
- 确定面试重点(首选目标放在面试后面)
了解面试官意图
<并发:线程池里发生异常怎么办?>
优化经验是高级工程师的常问问题
回答问题的STAR法则:
- 情境(Situation):所处什么情况?
- 任务(Task):接到了什么任务?
-
行动(Action):你怎么做的?
- 你怎么分析思考的?
- 选择什么方案?为什么?
- 你怎么进行方案落实的?
- 你怎么协调团队推进项目的?
- 你怎么监控结果的? --> 意识的体现
- 结果(Result):结果如何? --> 最好结合数值
简历
简历作用:
- 获得面试机会
- 作为面试参考
准备合适的简历:
- 基本信息简明扼要(寸土寸金)
- 求职意向清晰明了
- 根据JD定制简历(Base版本)
- 拿出你的项目成果(社招来讲,非常重要)
- 避免空洞的自我评价
简历的关键词
- 通过简历内容能够提取三个作用的关键词
- 关键词要与招聘需求匹配
简历的“四要、四不要”
- 要聚焦核心技能 不要到处熟练精通
- 要突出技术亮点 不要罗列开源框架(敢写就敢问,需要非常熟悉)
- 要体现业务背景 不要堆积项目细节(和招聘JD match)
- 要明确项目成果 不要陈述项目过程
回顾:
明确做简历的目标 -> 根据岗位需求准备简历 -> 抓住简历的关键意图
第3章 不假思索:我精通 Java
1.Java的char是两个字节,如何存储UTF-8字符?
--> 看似简单,其实暗藏玄机。很简单的活也能做出花来。
考察点:
- 是否熟悉Java char和字符串(初级)
- 是否了解字符的映射和存储细节(中级)
- 是否能触类旁通,横向对比其他语言(高级)
剖析题目技巧:
找名词,解释名词,关联名词
此题是:char、utf-8
char存储需要两个字节,utf-8需要1-3个字节(百度了下有中日韩长汉字有4个字节,待验证)
---> 所以是设置的陷阱,但要注意,不能说"题出错了",题目是给定范围,是话题作文,不是命题作文
题目剖析:
char是什么?utf-8是什么?占几个字节?和unicode有什么关系?(必须提unicode,因为char和utf-8和unicode关系很密切)
(unicode是字符集,字符集包括ASCII码和unicode,utf-8,utf-16,gbk之类是编码)
字符集的作用:把字符映射成整数,供计算机识别
---> 这个映射成的整数也叫码点,Java的String里面也可以看到一些codePoint的相关方法
如何存储字符?
存储是分两步的:
[人类感知的字符] --<映射>--> [字符集] (char) --<编码>--> [计算机存储] (2进制,byte)
---> 需要注意的是,计算机存储是以utf-16进行编码的(历史原因,65535不够用,这也是第二个要点)
小细节(小加分项):
理应是两个字节,但把字符串以utf-16编码,前面多了两个字节(是字节序的标志)
byte[] bytes = "中".getBytes("utf-8");
打印是4个字节
题外话:
Integer.toHexString() --> 映射的char
Integer.toBinaryString() --> 二进制
触类旁通:(我就先避免了,这个讲师对语言研究很深,毕竟是推广kotlin的)
因为python是解释执行的,源文件和执行时内存中的字符串内容是一致的(想一想:源文件开头就写了编码类型了)
编译过程中,Javac指定编码将字符串统一转为MUTF-8
令人迷惑的字符串长度:字符串长度!=字符数(现象,输入emoji 减2个字符)
emoji的长度为2,java9对latin字符的存储控件做了优化,但是字符串长度还是不等于字符数(python 3.3以上做了优化,长度就是字符串的长度)
---> 优化:字符串里如果只有用latin字符就可以表示的话,就可用byte来存,不用char(utf-16,占两个字节),节省一半的内存
题目结论:
- Java char不存utf-8的字节,而是utf-16的
- Unicode 通用字符集占两个字节,例如"中"
- Unicode 扩展字符集需要用一对char来表示,例如"😄"
- Unicode 是字符集,不是编码,作用类似于ASCII码
- Java String的 length 不是字符数,而是char的个数
技术点拨:
- 抓住细节,有技巧的回避知识盲区
- 把握节奏,不要等面试官追问
- 主动深入,让面试官了解你的知识体系
- 触类旁通,让面试官眼前一亮
<听到"我觉得"、"我认为"、"大概",面试官就来精神来摧毁你的知识盲区了>
2. Java String 可以有多长?
考察点:
- 是否对字符串编解码有深入了解(中级)
- 是否对字符串在内存当中的存储形式有深入了解(高级)
- 是否对Java虚拟机字节码有足够的了解(高级)
- 是否对Java虚拟机指令有一定的认识(高级)
题目剖析:
关键词:String、长
- 字符串有多长是指字符数还是字节数?
- 字符串有几种存在形式?
- 字符串的不同形式受到何种限制?
---> 题目没给出来,自己要想清楚,给面试官感觉候选人想问题比较全面,比较透彻
String构造:
- 直接赋值
---> 栈,编译器就能决定大小的 - 从文件的文本里读取byte,转成String
---> 堆,编译的时候无法预见,运行的时候还可能被改变
<step1:知道两种分配形式>
对于栈来说:TODO:03:26
.java源文件 编译成 .class字节码,存储String的数据结构是CONSTANT_Utf8_info,length是u2,表示两个字节,所以最大能存储65535个,
运行时,加载到虚拟机的方法区的常量池里
---> 此时常量池的大小就有一定限制,可控,通常不会连65535个字节都存不下,如果溢出会OOM
<step2:知道字节码里是utf8格式存储>
引申细节:
65535是理论上的,但实践发现65535个Latin字母会报错
--> 并不是结尾有\0来表示终止,java字节码里有存length
检查Gen.java的checkStringConstant()发现编译器的逻辑判断有问题。
--> 是javac的bug,kotlinc是没问题的
(手持两把锟斤拷,口中疾呼烫烫烫 -->滑稽)
---> 但是 65535/3 个汉字经过Mutified_utf_8是可以的
-----> 因为汉字需要经过utf-8编码这种没办法直接知道多大,需要先进行编码再判断长度,此时逻辑是正确的(writePool.java)
栈部分String存储总结:
- 受字节码限制,字符串最终的MUTF-8字节数不能超过65535
- Latin字符,受Javac代码限制,最多65534个
- 非Latin字符最终对应字节个数差异较大,最多字节个数是65535
- 如果运行时方法区设置较小,也会收到方法区大小的限制
堆部分:
收到虚拟机指令 newarray [int] 的限制,数组理论最大个数为Integer.MAX_VALUE
---> ArrayList.java的MAX_ARRAY_SIZE有注释说明,实际实现上会小于Integer.MAX_VALUE
String在栈部分存储总结:
- 受虚拟机指令限制,字符数理论上限为Integer.MAX_VALUE
- 受虚拟机实现限制,字符数理论上限为Integer.MAX_VALUE
- 如果堆内存较小,也会受到堆内存的限制
<step3:分析的非常完善了,包括字节码、编译器、内存、虚拟机指令等>
本节回顾:
- Java String 字面量形式
- 字节码中CONSTANT_Utf8_info的限制
- Javac源码逻辑的限制
- 方法区大小的限制
- Java String 运行时创建在堆上的形式
- Java 虚拟机指令 newarray 的限制
- Java 虚拟机堆内存大小的限制
技巧点拨:
思路很重要!
- 这种类型的题目最终结果往往不重要
- 拿到问题,知道如何分析,知道从哪儿分析是关键
--> 比如javac的bug,其实知道又不能更香一点,面试官更想在你不知道的情况下,看你如何思考。如果慌了不行,说明心理因素不行,实际工作中交予有挑战性的工作也扛不住。
切不可眼高手低! - 简单的问题背后暗藏玄机
- 尽一切可能将题目引向自己擅长的领域
拓展:注解有几种用法?不是"茴香豆"的"茴"有几种写法的问题,其实对细节得有方方面面的了解,才好全面的实现
---> 感觉上,这是讲师的主观想法了,看架构系列书又说得取舍的,毕竟讲师的深入开发语言研究的。不同人看问题角度不同。
推荐两本书《Java虚拟机规范》、《Java语言规范》
3. Java匿名内部类有哪些限制?
考察点
- 考察匿名内部类的概念和用法(初级)
- 考察语言规范以及语言的横向对比等中级)
- 作为考察内存泄漏 的切入点(高级)
关键词:匿名内部类、限制 --> 讲出概念就通过了
--> 回答问题要找主谓宾:看来语文也很重要(笑哭)
在类里面new一个就是,很常用,比如setOnClickListener的OnClickListener就是
- 匿名内部类的名字:外部类加$N,N是匿名内部类的顺序
--> 有名字,但是human not readable的
(step1:了解java的编译) - 匿名内部类的继承结构:
<java 10的类型推导var也不能实现匿名内部类既继承接口又实现接口 >
方法内部可以实现既继承接口又实现接口,但其实已经不是匿名内部类了
---> 触类旁通:kotlin是可以实现既继承接口又实现接口的
(step2:平时很少有人注意到,但是说明对这门语言掌握的透彻,下次需要涉及相关问题就减少试错成本) - 匿名内部类的构造方法:(比较重要,可以引申内存泄露)
匿名内部类的构造方法是怎么定义的? --> 是编译器定义的,开发者没有权限,原因在于它会悄悄的给你引入一些东西
悄悄引入的东西:- 情形1:非静态内部类(在外部相当于一个成员变量)
- 自己的外部类的实例;2.非静态父类的外部类实例
- 情形2:静态内部类(interface也是)
外部是在非静态方法调用:自己的外部类的实例;
外部是在静态方法调用:自己的外部类的实例也没有了 - 情形3: 捕获外部变量,前提必须是final的,如果不是final就会被重新赋值了,本质上是复制一份作为构造函数(快照)
<在对象没有被重新赋值的情况下,自动被final了>
总结:---> 实践回顾下
- 编译器生成
- 参数列表包括:
- 外部对象(定义在非静态域内)
- 父类的外部对象(父类非静态)
- 父类的构造方法参数(父类有构造方法且参数列表不为空)
- 外部捕获的变量(方法体内有引用外部final变量)
(step3:对匿名内部类了如指掌了)
- Lambda转换(SAM类型 --> Single abstract method)
- 要求是接口,并且只有一个方法(不然Lambda表达式作为函数类型怎么知道替代哪个方法?)
---> 展现聪明好学,积极向上
- 要求是接口,并且只有一个方法(不然Lambda表达式作为函数类型怎么知道替代哪个方法?)
本节回顾:
- 没有人类认知上的名字
- 只能继承一个父类或实现一个接口
- 父类是非静态的类型,则需父类外部实例来初始化 --> 不算很特色,非匿名内部类也是一样的
- 如果定义在非静态作用域内,会引用外部类实例
- 只能捕获外部作用域内的final变量
- 创建时只有单一方法的接口可以用Lambda转换
技巧点拨:
- 关注语言版本的变化
- 体现对技术的热情
- 体现好学的品质
- 显得专业
巩固:
实例代码
public class Client {
public void run(){ // 尝试改为静态方法
// final Object object = 1; // 尝试替换成int、String
OuterClass.InnerClass innerClass = new OuterClass().new InnerClass(){ // <眼前一亮:两个new>
@Override
void test() {
// System.out.println(object);
}
};
}
}
public class OuterClass { // 尝试加构造函数
public abstract class InnerClass{ // 尝试改为interface
abstract void test();
}
}
--> 查看编译成的Client$1.class
<和上面结论可以对应上了,并且外加外部捕获需不是基本类型>
4. 怎样理解Java的方法分派?
考察点:
- 多态、虚方法表的认识(初级)
- 对编译和运行时的理解和认识(中级)
- 对Java语言规范和运行机制的深入认识(高级)
- 横向对比各类语言的能力(高级)
- Groovy、Gradle DSL 5.0以前唯一正式语言
- C++、Native程序开发必备
题目剖析:
关键词:方法分派
- 就是确定调用谁的、哪个方法
- 针对方法重载的情况进行分析
- 针对方法覆写的情况进行分析
经常可见的继承相关题目,
问:程序输出什么? <覆写>
---> 取决于运行时的实际类型
(step1:知道方法分派调用谁)
问:程序如何运行? <重载>
---> 编译时确定,而且是必须要确定的,不然编译就出错了(看字节码就可以知道)
(step2:知道方法分派调用哪一个)
Java 方法分派
- 静态分派 - 方法重载分派
- 编译期确定
- 依据调用者的声明类型和方法参数类型
- 动态分派 - 方法覆写分派
- 运行时确定
- 依据调用者的实际类型分派
触类旁通:
Groovy和Java的不同,在运行的时候,是选择运行时的实际类型,因为生成的字节码是CallSite.callStatic,在运行的时候根据反射拿到实际类型决定调用哪一个
---> Groovy是动态语言
C++ 虚方法需要显示指定,且实例会被减裁,所以非指针形式调用的还是父类的(指针需要直接传入,或者运行接收引用)
---> C++选择空间很大
(step3:1+1>2,会引发很多思考)
回顾:
- 分析Java方法重载时的分派行为
- 分析Java方法覆写时的分派行为
- 横向对比Groovy与C++的分派行为
技巧点拨:
- 横向对比 ---> 语言跨度不够大..GG
- 体现扎实的语言基本功
- 体现对编程语言特性的钻研精神 ---> 得多看看分析对比博客,实践一下
- 显得专业
---> 体现了极客精神
5. Java 泛型的实现机制是怎样的?
考察点:
- 对Java泛型使用是否仅停留在集合框架的使用(初级)
- 对Java泛型的实现机制的认知和理解(中级)
- 是否有足够的项目开发实战和“踩坑”经验(中级)
- 对泛型(或模板)编程是否有深入的对比研究(高级)
- 对常见的框架原理是否有过深入剖析(高级)
<函数的返回值里面有泛型参数,编译完以后,函数的签名是怎样的?>
题目剖析
- 题目区分度非常大
- 回答需要提及以下几点才能显得更有亮点:
- 类型擦除从编译角度的细节
- 类型擦除对运行时的影响
- 类型擦除对反射的影响
- 对比类型不擦除的语言
- 为什么Java选择类型擦除
- 可从类型擦除的优劣来着手分析回答
类型擦除有哪些好处?
- 运行时内存负担小
以List<T>为例,类型擦除了,运行时内存中一个List即可搞定所有事情
--> 因为运行时需要加载到方法区,所以对方法区的压力有一定的影响 - 兼容性好(前泛型时代:Raw类型) --> 思考思考
(step1:知道Java泛型怎么实现的)
泛型擦除有哪些问题?
- 基本类型无法作为泛型实参 --> 涉及到装箱拆箱问题的开销(为了规避拆装箱问题,Google特意出了SparseArray)
- 泛型类型无法用作方法重载 --> 编译完,实际上两个一样了
- 泛型类型无法当作真实类型使用 --> T不是真实的类型,没法new,也没法获取类型作为类型判断
--> 可以用于其他泛型类型,反正要擦除(List<T> list = new ArrayList<T>())
知识迁移:Gson.fromJson为什么需要传入Class?
--> 因为编译完实际上返回的是Object,不传入不知道返回的确定对象 - 具体方法无法引用类泛型参数 --> 方法的泛型参数,只有在类实例化后才能确定,但是静态方法无需类的实例
--> 静态方法可以声明泛型参数:public static <R> R max(R a, R b){...} - 类型强转的运行时开销
- 1.5之前需要手动强转
- 1.5之后编译器帮你强转了
(step2:已经对泛型有很完备的认识了)
补:使用协变和逆变,
- 类型擦除,为了保证安全性,所以不能协变了,引入了通配符"? extends",但是只能用它不能修改它 --> 应用:比如打印数组(只能输出)
- ? super 是逆变:使用和? extends相反,比如要View放到一个TextView的List里(只能输入)
<单单?表示没有上界和下界>
---> kotlin的out和in更直观的描述了
附加的签名信息:
<实际编程中很少有使用场景,但通过反射去拿还是有些有意思的事情的>
---> 反射拿actualTypeArgument,拿到真实的类型,在写一些框架上很有用
(getGenericXXXType:拿到真实的类型) --> generic是运行时的意思?
所以,注意混淆的时候要保留签名信息<-keepattributes Signature> --> 不说不知道啊
迁移:
- Gson:用的地方很多
TypeToken声明的是protect,不能直接new,但可以构造匿名内部类 --> 研究一下
--> 因为匿名内部类是TypeToken的子类,子类可以访问父类的构造方法,就可以拿到泛型实参了 - Retrofit
--> 框里实现也是通过getGenericReturnType来获取泛型的实际类型
----> 高级工程师,需要写框架的人才,只用过是初级,是码农 - 进一步迁移:Kotlin 反射的实现原理
通过Metadata注解,注意混淆的时候要保留(-keep class kotlin.Metadata{*;})
<Java里反射去获得签名信息的时候,实际上是通过一个附加的签名,这个签名在运行时除了反射没有别的作用,可以考虑不混淆>
(step3:融会贯通,很系统化,优秀优秀)
回顾:
- Java泛型采用类型擦除实现
- 类型编译时被擦除为Object,不兼容基本类型(基本类型和Object不是同一个线上的东西 --> SparseArray)
- 类型擦除的实现方案主要考虑后向兼容(用户众多,不得已,不然线上都crash就很严重了) --> 像C#是真实实现泛型的
- 泛型类型签名信息特定场景下可通过反射获取
技巧点拨:
- 结合项目实践(旁征博引)
- 阐述观点给出实际案例,例如Gson、Retrofit --> 给人感觉不仅知道原理,甚至还能写出更好的,能根据实际场景定制(需要知道用的东西是否真正了解了)
- 实战中经常需要混淆,有哪些注意点以及原理
(混淆很容易问出彩,关注的是字节码,字节码存的名字怎么存,名字和符号的引用是什么关系)
6. Activity 的 onActivityResult 使用起来非常麻烦,为什么不设计成回调?
考察点:
- 是否熟悉 onActivityResult 的用法(初级)
- 是否思考过用回调替代 onActivityResult(中级)
- 是否实践过用回调替代 onActivityResult(中级)
- 是否意识到回调存在的问题(高级)
- 是否能给出匿名内部类对外部引用的解决方案(高级)
题目剖析:
关键词:onActivityResult、回调
- onActivityResult 是干什么的,怎么用
- 回调在这样的场景下适用吗?
- 如果适用,那为什么不用回调?
- 如果不适用,给出你的理由
-
onActivityResult 为什么麻烦?
- 代码的处理逻辑分离,容易出现遗漏和不一致的问题 (期待activity返回数据) --> startActivityForResult、onActivityResult、setResult 分离
- 写法不够直观,且返回数据没有类型安全保障 <setResult里使用的是Intent,类似HashMap>
- 结果种类较多时,onActivityResult就会逐渐臃肿难以维护 <返回的data是Intent,是以bundle形式存储的,必须看到setResult到底是什么才能知道>
<step1:明白onActivityResult麻烦在哪>
-
假设:使用回调实现:高内聚(上述1,3缺点没了)
其实就不能使用,但Activity的销毁和恢复机制不允许匿名内部类出现 --> 引用变换了(传回时是AMS重新new出来的)
(step2:思考的还是蛮透彻的) -
基于注解处理器和Fragment的回调实现:(开源:
RxPermissions、ActivityStarter --> 研究一下) TODO:12:24
Fragment里面也有onActivityResult,可以尝试替代Activity里的onActivityResult,考虑添加一个空的Fragment,包装一个触发,就可以实现一个接口使用回调
- 返回参数类型可以可以基于注解器,让参数类型和接收方onResult的参数类型强相关,就解决了返回结果类型不安全的问题
- 解决引用变换的问题:
可以在activity更新的时候替换成新的引用
---> 新引用从哪来:被调用的时候,fragment已经是更新了的,它持有的activity已经是更新后的activity(替换匿名内部类的外部引用,通常为$this)
---> 外部的自由变量(比如View):通过构造方法捕获的外部的局部变量(副本)也需要更新,需要找新的activity的对应的它
----> 通过id?但是如果写的不好,可能会重复,在saveInstance和restore的时候也会有问题<拿最新的activity来findViewById>
----> 类似的,对应fragment怎么更新?id是container的Id,多个fragment不唯一;tag可有可无,可重复,也不可靠;(捕获Fragment的引用通过字段mWho来替换,但不是公开的,通过反射来拿)
(step3:思考已经很深入了)
<思考:捕获了View和Fragment以外的变量需要替换吗? ---> 印象中插件化也有考虑>
回顾:
- 分析onActivityResult在使用上存在的问题
- 分析为什么onActivityResult的场景不设计成回调
- 通过替换匿名内部类的外部引用实现回调
不是浅尝辄止,哪怕看上去不可能,也要去尝试,让项目具有一定的可能性。比如热更新、插件化之类,经常需要Hook,Hook失败率很高,但是不断尝试,不断挑战不可能,尽可能让方案适应大多数场景。
技巧点拨:
- 多想为什么
- 多实践自己的想法 --> 让想法形成一个实体在发光
第4章 不可轻敌:我真的熟悉并发编程吗?
1. 如何停止一个线程?
<题外话:怎么检查static的对象是否过大?对象能否看到是怎么创建的?>
考察点:
- 对线程的用法有了解(初级)
- 是否对线程的stop方法有了解(初级)
- 是否对线程stop过程中存在的问题有认识(中级)
- 是否熟悉interrupt中断的用法(中级)
- 是否能解释清楚使用boolean标志位的好处(高级)
- 是否知道interrupt底层的细节(高级)
- 通过该题目能够转移话题到线程安全,并阐述无误(高级)
题目剖析:
关键词:停止、线程
-
官方停止线程的方法被废弃,所以不能直接简单的停止线程(直接停止,锁之类的很不安全)
-
如何设计可以随时被中断而取消的任务线程(逻辑上停止)
-
为什么不能简单的停止一个线程?
因为如果线程本来是加锁在操作内存/文件的,突然停止,来不及做善后工作,会导致当前的内存/文件的数据很可能变得混乱(形成一个烂摊子,比如打开文件肯定要关闭了),其他线程操作内存/文件的时候很有可能也发生crash(操作内存的情况导致的问题比较多)
---> 因此大部分语言设计的线程都是不可停止,甚至暂停的。
(step1:对锁之类概念还是很熟悉)
<线程的设计往往是和任务强绑定的>
-
协作的任务执行模式
- 通知目标线程自行结束,而不是强制停止
- 目标线程应当具备处理中断的能力
- 中断方式
- Interrupt
- boolean 标志位
-
interrupt的原生支持
(比如:sleep方法需要捕获)
---> 支持interrupt,在任务执行的循环节点判断是否需要中断,进行善后操作 -
interrupted() 与 isInterrupted()
- interrupt()是静态方法,获取当前线程的中断状态,并清空
- 当前运行的线程
- 中断状态调用后清空,重复调用后续返回false
- isInterrupted()是非静态方法,获取该线程的中断状态,不清空
- 调用的线程对象对应的线程
- 可重复调用,中断清空前一直返回true
(原理是native层,和我们平时设置boolean没区别)
---> 为什么interrupted()要清空当前中断状态
(step2:原理简单,却是亮点)
- interrupt()是静态方法,获取当前线程的中断状态,并清空
-
boolean标志位:其实和interrupt原理和用法一致,一个native,一个java而已
---> 设定的标志位具有线程间可见性问题,需要加volatile,表示这个是易变的(详细需要内存模型)
interrupt与boolean标志位对比
interrupt | boolean标志位 | |
---|---|---|
系统方法(sleep) | 是 | 否 |
使用JNI | 是 | 否 |
加锁 | 是 | 否 |
触发方式 | 抛异常 | 布尔值判断,也可抛异常 |
---> framework很多是通过抛异常来实现的,比如startActivity
- 需要支持系统方法时用中断(功能性) --> 比如sleep方法必须绑定interrupt
- 其他情况用boolean标志位(性能) --> 使用JNI有开销
(step3:在选型有优化意识,并且掌握的很好)
回顾:
- 为什么线程不能被直接stop --> 得给线程喘息的时间
- 线程内置中断的使用与原理
- 通过volatile boolean标志位通知线程停止
----> 要多读源码,其实有些东西也很简单
2. 如何写出线程安全的程序?
考察点:
- 是否对线程安全有初步了解(初级)
- 是否对线程安全的产生原因有思考(中级)
- 是否知道final、volatile关键字的作用(中级)
- 是否清楚1.5之前Java DCL为什么有缺陷(中级)
- 是否清楚的知道如何编写线程安全的程序(高级)
- 是否对ThreadLocal的使用注意事项有认识(高级)
题目:
关键字:线程安全
- 什么是线程安全?
- 如何实现线程安全?
Java的内存模型:TODO:02:40
<(每个线程)工作内存 + 主内存>
什么是线程安全?
- 可变资源(内存)线程间共享
--> 可变、共享才有线程安全问题
---> 为什么不关注"进程安全":进程之间一般来说只共享CPU时间片,内存是独享的,如果进程被杀掉,它所有的内存都还给物理内存了。
(step1:知道了什么是线程安全)
** 如何实现线程安全?**
- 不共享资源
- 共享不可变资源
- 共享可变资源
- 可见性
- 操作原子性
- 禁止重排序
不共享资源
可重入函数:纯函数,函数式编程先天具有线程安全优势
不共享资源
ThreadLocal:虽然每个线程都会去访问一个值(确实不好表述),最终访问的是自己线程内部的副本。
比如每个用户都要不同的token,那就都开个线程,UUID获取,每个线程的副本是不共享的,就可以实现了。
深入理解ThreadLocal
---> ThreadLocal的map是绑定到线程上的
对比:ThreadLocalMap和WeakHashMap很像
ThreadLocalMap | WeakHashMap | |
---|---|---|
对象持有 | 弱引用 | 弱引用 |
对象GC | 不影响 | 不影响 |
引用清除 | 1.主动移除 2.线程退出时移除 | 1.主动移除 2.GC后移除(ReferenceQueue) |
Hash冲突 | 开发地址法 | 单链表法(1.8后加入红黑树) |
Hash计算 | 神奇数字的倍数 | 对象hashCode再散列 |
使用场景 | 对象较少 | 通用 |
--> 弱引用:没人引用,GC的时候是可以移除的
---> 因为ThreadLocalMap使用对象较少,没必要去监听ReferenceQueue(监听也是有一定的开销的)
----> WeakHashMap GC后会存到ReferenceQueue,监听它,去移除
ThreadLocal的使用建议
- 声明为全局静态final成员
--> 线程里面有一个实例就够了,设置value的时候是以ThreadLocal为key的,如果不断变换,设置进去永远都找不到的(另外,可以改它还会有可见性问题,什么时候初始化?) - 避免存储大量对象 --> 底层数据结构导致(开放地址法+清除策略+hash计算)
- 用完以后及时移除对象 --> 自己没有监听机制,会导致线程如果一直存在,引用一直不会被移除
共享不可变资源
final还有禁止重排序的作用
---> 比如,有些虚拟机的实现和CPU架构,非final的成员的赋值会被重排序到构造方法之外,有可能构造方法完了,但是赋值还没过完,此时访问是未初始化的
volatile:常见单例的双加锁
<不加重排序的问题和不加final类似,锁失效了> ---> 原理是汇编
(step2:知道禁止重排序,禁止共享可变资源)
保证可见性
保证可见效性的方法:
- 使用final关键字
- 使用volatile关键字
- 加锁,锁释放时会强制将缓存刷新到主内存(加锁才会从主内存中更新)
保证原子性
保证保证原子性的方法: --> 操作的时候要保证原子性(比如++操作是可拆分的,任何语言实现都是把值从temp里读出来,再加1,再写回去)
- 加锁,保证操作的互斥性
- 使用CAS指令(如Unsafe.compareAndSwapInt) --> Unsafe不是公开的,需要反射
- 使用原子数值类型(如AtomicInteger)
- 使用原子属性更新器(AtomicReferenceFieldUpdater)
(step3:对线程安全的体系非常清楚)
回顾:
- 阐述了什么是线程安全
- 如何编写线程安全的程序
- 不变性(能不变就不变,就没有线程安全问题)
- 可见性
- 原子性
- 禁止重排序
3. ConcurrentHashMap 如何支持并发访问?
考察点:
- 是否熟练掌握线程安全的概念(高级)
- 是否深入理解CHM的各项并发优化的原理(高级)
- 是否掌握锁优化的方法(高级)
ConcurrentHashMap是个持续的过程,不断优化。
题目剖析:
关键词:ConcurrentHashMap、并发访问
- 并发访问即考察线程安全问题
- 回答ConcurrentHashMap原理即可
如果你对CHM的原理不了解
- 分析下HashMap为什么不是线程安全的(可以提一下HashTable)
- 编写并发程序时你会怎么做,举例说明最佳
CHM的并发优化历程
JDK5 :分段锁,必要时加锁
JDK6 : 优化二次Hash算法
JDK7 : 懒加载,volatile & cas --> 尽量避免加锁
JDK8 : 摒弃段,基于HashMap原理的并发实现 --> 尽量选择很小的范围去加锁
CHM分段锁的hash优化(JDK6):TODO:04:00
--> 需要让hash(key)散列均匀,不然就退化成了一个HashTable
(step1:知道了jdk5到jdk6的一个优化,虽然差异很小,但是优化很大)
CHM分段懒加载(JDK7)
JDK7 之前Segment直接初始化
JDK7 Segment 使用时初始化
---> 实例化的过程中,为了保证Segment的可见性,JDK7里对数组进行了大量的volatile(getObjectVolatile)
CHM摒弃分段锁(JDK8) --> 1.8是没有分段的
直接用getObjectVolatile来访问table,加锁只针对Table里Slot对应的Entry,把新来的元素放在它的链表里
(step2:了解了不同版本的差异)
CHM如何计数 --> 什么是计数
- JDK5~7基于段元素个数求和,两次不同就加锁再求一次
- JDK8引入CounterCell,本质上也是分段加锁
CHM是弱一致性的
- 添加元素后不一定马上能读到 --> 添加的时候可能已经读过去了
- 清空之后可能仍然会有元素 --> 清是一段一段的清,刚清后就添加了,还要去清后面的
- 遍历之前的段元素的变化会读到
- 遍历之后的段元素变化读不到 --> 比如当前遍历到14,13发生改变读不到,15发生改变,就可以感觉到这个变化
- 遍历时元素发生变化不抛异常 --> 遍历是经常的
HashTable的问题
- 大锁:对HashTable对象加锁 --> 加到了类上
- 长锁:直接对方法加锁
- 读写锁共用:只加一把锁,从头锁到尾
针对这些问题
CHM的解法
- 小锁:分段锁(5~7),桶节点锁(8)
- 短锁:先尝试获取,失败再加锁
- 分离读写锁:读失败再加锁(57),volatile读CAS写(78)
如何进行锁优化?
- 长锁不如短锁:尽可能只锁必要的部分
- 大锁不如小锁:尽可能对加锁的对象拆分 --> 感觉和第一点重复了
- 公锁不如私锁:尽可能将锁的逻辑放到私有代码中 --> (逻辑上)对外不保留,内部加锁,避免外部可调用导致死锁
- 嵌套锁不如扁平锁:尽可能在代码设计时避免锁嵌套
- 分离读写锁:尽可能将读锁和写锁分离 --> 读和写的频次不一样,往往读很多,那么写要加一个很重的锁,读volatile即可,甚至可以不加
- 粗化高频锁:尽可能合并处理频繁过短的锁 --> 加锁和释放锁非常频繁
- 消除无用锁:尽可能不加锁,或用volatile(和CAS)替代锁
(step3:分析的很透彻,总结出方法论<方法论的建设是高工必不可少的>)
技巧点拨:
- 话题关联:转移话题到你熟悉的领域
- 紧跟时代:避免因知识陈旧丢分
- 学以致用:他山之石,可以攻玉
4. AtomicReference和AtomicReferenceFieldUpdater有何异同?
考察点:
- 是否熟练掌握原子操作的概念(中级)
- 是否熟悉AR和ARFU这两个类的用法和原理(高级)
- 是否对Java对象的内存占用有认识(高级)
--> Java虚拟机本身不想让我们对内存有认识,所以Java程序员天生有了短板 - 是否有较强的敏感度和深入探究的精神(高级)
题目剖析:
关键词:AtomicReference、AtomicReferenceFieldUpdater
AtomicReference利用CAS(compareAndSet)
AtomicReferenceFieldUpdater:volatile 修饰变量 + CAS + 像反射的调用
(step1:会用)
AR和ARFU的对比
看似AR比ARFU使用起来方便很多,但是实际上框架里编写用ARFU更多。
---> 实际上AR里也有volatile修饰的value(万变不离其宗嘛)
差异在于AtomicReference本身也创建了一个对象,所以比ARFU多出来16个字节,头(Header):12B,成员(Fields):4B。(所以使用ARFU节省内存,减少内存消耗缓解GC压力,不要小看16B,成千上万的使用就很多了)
(step2:知道了AR和ARFU的根本性差异了 /)
更完善一点:
- 对于32位的是这样的情况
- 对64位:
- 启用了指针压缩(-XX:-UseCompressedOops),也是16个字节;
- 没有启用指针压缩的话,每个引用占8个字节,所以Header:16B,Fileds:8B,Total:24B
(往往融合到线程安全里考察的,提到原子性把这一点提出来,就是加分项,症结内存问题一提就很赞了)
迁移:使用ARFU的例子
- JDK里BufferedInputStream,里面就定义了bufUpdater来更新buf,虚拟机里会创建很多BIS实例,如果每个实例都用AR,每个就会多16/24个字节内存
- kotlin的lazy的PUBLICATION的实现也是ARFU(得学得学)/kotlin协程的实现,也涉及原子性的操作,也用到了ARFU
(step3:还能引用实例,那就知道了选择的场景,比如单例可以用AR)
题目结论
- AR和ARFU的功能一致,原理相同,都基于Unsafe的CAS操作
- AR通常作为对象的成员使用,占16B(指针压缩),24B(指针不压缩)
- ARFU通常作为类的静态成员使用,对实例成员进行修改 --> 基本上是对它的实例进行反射更改
- AR使用更友好,但是ARFU更适合类实例比较多的场景 --> 写网络框架肯定用ARFU了
技巧点拨
- 知识迁移:举个例子,最好能结合自身经验 --> 或者源码
(表现聪明,肯钻研,有意识去思考和实践 --> 高工需要能造轮子的)
5. 如何在Android中写出优雅的异步代码?
考察点:
- 是否熟练编写异步和同步的代码(中级)
- 是否熟悉回调地狱(中级)
- 是否能够熟练使用RxJava(中级)
- 是否对kotlin协程有了解(高级)
- 是否具备良好代码的代码意识和能力(高级)
题目剖析:
- 什么是异步?
- 如何定义优雅?
什么是异步
代码不按书写的方式执行,部分代码在一定条件下才触发返回(比如setOnClickListener也是异步)
---> 异步不一定有多个线程
异步的目的是什么
- 提高CPU利用率
- 提高GUI程序的响应速度
- 异步不一定快!
---> 如果是CPU密集型任务,无论是异步还是高并发,切换线程的时候会有一定开销(所以耗时任务异步,IO密集型)
回调地狱
异步里面套异步 --> JS里更严重
(step1:对异步和同步的概念有了解)
---> 解决回调地狱
RxJava基本用法
通过运算符把异步回调过程的过程给打平了(内部可能也会有回调地狱的问题,但给用户呈现出来的就是链式调用)
---> 加上Lambda表达式就会很简洁
Lambda表达式的简洁带来的问题
RxJava异常处理
一旦出现异常,如果不处理,就会crash了。
所以一定要实现onError(传入Observer,把能处理的都处理了)
---> 但是这样又不美观了,又搞出来匿名内部类了。
解决方法:
- subscribe前面加上onErrorReturnItem把异常映射成某个类型的response(逻辑统一,简洁美观)
- 有些异常没法映射成正常的结果,那就用RxJavaPlugins.setErrorHandler来捕获并伤疤片
- 注意事项(也是优化),里面很可能是OnErrorImplementedException,上报里面的getCause即可(不然会有大量的无用信息)
- 如果是很严重的异常,那就用Exception.throwIfFatal(e)抛出来
RxJava取消处理
原因:RxJava执行异步任务,有可能已经被销毁,本质上都是匿名内部类
--> Q1:匿名内部类持有外部的引用,会导致Activity被持有,不能回收(如果迟迟不返回,就会造成内存泄露)
--> Q2:UI已经被销毁,可能出现空指针
策略:
- 使用Disposeable:
- 声明Disposeable列表
- 每次创建的时候,存储Disposeable对象
- 在onDestroy的时候清理Disposeable对象(调用dispose方法)
---> 逻辑简单,但使用体验较差(抽象到BaseActuvity)
- 利用uber出的.as(AutoDispose.autoDisposeable(ViewScopePrivider.from(button)))监听View状态自动取消订阅
--> 转成它包装的RxJava对应对象,根据他的生命周期来自动取消订阅
(step2:亮点,考虑问题很细致)
kotlin协程
好处:看起来和同步代码几乎一摸一样,连链式都没有了(感觉和dart也很像) --> await() suspend等待
将回调转换为协程的挂起函数,可以认为:挂起函数之后的部分运行在回调中
button.onClick{
val req = Request()
val resp = async{sendRequest(req)}.await()
updateUI(resp)
} --> 异常处理直接try...catch
onClick原理<利用编译器的语法糖>:给View实现了一个扩展方法,启动了一个协程,setOnClickListner
--> 仿照ubser的AutoDisposable,在detachWindow的时候取消
(step3:不仅能用轮子,还能根据自己的idea造轮子 --> 如果自己造过轮子千万要告诉面试官)
本节回顾:
- 回调地狱是可怕的,可读性差,容易出问题
- 使用RxJava将异步逻辑扁平化,注意异常处理和取消处理
- 使用Kotlin协程将异步逻辑同步化,注意异常处理和取消处理
---> 协程和RxJava都是好的选择,取决于项目的技术栈
第5章 不求甚解:让我们挖一挖 JNI 编程的细节
1. CPU架构适配需要注意哪些问题?
- 是否有Native开发经验(中级)
- 是否关注过CPU架构适配(中级)
- 是否有过含Native代码的SDK开发的经历(中级)
- 是否针对CPU架构适配做过包体积优化(高级)
---> 尽量往优化上扯
题目剖析:
关键词:CPU架构
- Native开发才会关注CPU架构
- 不同CPU架构之间的兼容性如何
- so库太多如何优化Apk提及
- SDK开发者应当提供哪些so库
---> 这是一个很好的Native话题的切入点
CPU架构的指令兼容性 TODO:02:34
Native库加载
提供so库一定要提供一整套,会到对应目录下去加载(没有的话,就算有兼容的也会加载异常)
--> 要么一个都不提供(这里是针对目录的)
兼容模式运行的一些问题
- 兼容模式运行的Native库无法获得最优性能
- 所以x86的电脑上运行arm的虚拟机会很慢 (官方建议用x86的虚拟机,但是x86的so库比较少)
- 兼容模式容易出现一些难以排查的内存原因 (闲鱼的flutter库就有)
- 系统优先加载对应架构目录下的so库 (要么整套,要么都没有)
(step1:有native开发经验)
为App提供不同架构的Native库
为了包体积肯定不能全部提供
- 性能不敏感且无运行时异常:armeabi是兼容性最好的 2. 结合目标用户群体提供合适的架构:大部分机器都是v7a和v8a,提供v7a就可以了
- 一个目录下可以提供其他类型的so库,比如armeabi-v7a目录下,为了更好的兼容性能放libmath_v8a.so(可能有大量的数学运算),程序就可以动态的获取了(微信就是这样做的)
---> 线上监控问题,针对性提供Native库(只有不断接受反馈的数据,才能确定为哪个soku提供兼容性版本)
(step2:知道so库的加载规则)
- 一个目录下可以提供其他类型的so库,比如armeabi-v7a目录下,为了更好的兼容性能放libmath_v8a.so(可能有大量的数学运算),程序就可以动态的获取了(微信就是这样做的)
动态加载Native库
一些比较大的so库,直接打到apk里会导致非常大(有些可能就用不到,可交由用户开启功能再下载)
---> 非启动加载的库可从云端下发
优化so体积
---> 大部分android开发对C/C++不熟悉
- 默认隐藏所有符号,只公开必要的
- -fvisibility=hidden
- 禁用C++ Exception & RTTI(开发用不到,但会导致so库很大)
- -fno-exception -fno-rtti
- 不要使用iostream,应优先使用Android Log
- 使用gc-sections去除无用代码 --> 像Java的混淆
- LOCAL_CFLAGS += -ffunction-sections -fdata-sections
- LOCAL_LDFLAGS += -Wl,--gc-sections
构建时分包
官方提供,不少应用商店已经支持按CPU架构分发安装包
splits {
abi{
enable true
reset()
include "armeabi-v7a","armeabi-v8a","x86","x86_64"
universalApk true
}
}
SDK开发者需要注意什么 --> SDK开发没有做到极致,因为面临环境更苛刻
- 尽量不要在Native层开发,降低问题跟踪维护成本
- 尽量优化Native库的体积,降低开发者的使用成本 --> <流量分发是要钱的>
- 必须提供完整的CPU架构依赖 (有些不兼容可是会crash的)
(step3:说明有这方面的经验)
本节回顾:
- 注意CPU架构指令兼容关系,以及兼容模式下的问题
- 注意合理提供Native库,在性能和体积上取舍
- 提过代码编写和编译的手段缩减Native库的体积
- 通过动态下发的方式减少安装包的体积
- SDK开发相比App开发者应当多关注的一些问题
技巧点拨:
- 聚焦热点:任何题目一旦涉及优化,必然拔高(显得考虑周到,经验丰富)
2. Java Native方法与Native函数是怎么绑定的?
考察点:
- 是否有Native开发经验(中级)
- 是否面对知识善于发现背后的原因(高级)
题目解析:
关键词:Java Native方法、Native函数
TODO:00:40
- 静态绑定:通过命名规则映射
- 动态绑定:通过JNI函数注册
静态绑定:
- 根据报名,点换成下划线
- 第一个参数JNIEnv,如果是静态的是jclass,非静态的是jobject(表示方法的引用对象)
(step1:懂点,有兴趣继续聊下去了) - extern "C":告诉编译器编译这个名字的时候一定要按C的规则
- JNIEXPORT:优化so库的时候,尽可能让native函数隐藏,不是所有的函数都需要在符号表里出现的。JNIEXPORT强制把它丢到DEFAULT里,让它可见
- JNICALL:有些平台上(比如mips、windows)可以告诉编译器这个函数调用的惯例是什么,参数是怎么入栈的,返回了这个栈是怎么清理的,主要是兼容性问题
(step2:对底层开发有一定的了解)
<题目并不简单:比如Activity和Fragment有什么区别?看似简单,其实很有料,要把知道的全部答出来,不要吝啬,比如Activity怎么启动的,插件化遇到的问题>
动态绑定
没有那么多麻烦事,直接一个函数调用,registerMethods把方法传进去。
特点:
- 动态绑定可以在任何时刻触发(可以进行替换、甚至取消)
- 动态绑定之前根据静态股子额查找Native函数
- 动态绑定可以在绑定后的任意时刻取消
---> 很有可能作为一些有技巧性的框架的Hook点
动态和静态绑定对比
动态绑定 | 静态绑定 | |
---|---|---|
Native函数名 | 无要求 | 按照固有规则编写且采用C的名称修饰规则 |
Native函数可见性 | 无要求(可优化) | 可见 |
动态替换 | 可以 | 不可以(写死了) |
调用性能 | 无需查找 | 有额外的查找开销(开销不大) |
开发体验 | 几乎无副作用 | 重构代码时较为繁琐(两个地方都得改) |
Android Studio支持 | 不能自动关联跳转 | 自动关联JNI跳转(小箭头一点) |
---> 归纳的好
(可见效上有个so库体积的优化点,全用动态绑定)
(step3:有总结规范的想法,不零散,体系化 --> 还能作为榜样,起到带动作用)
本节回顾:
- Native方法绑定主要有动态和静态两种
- 静态绑定主要so库的符号表以及函数的名称修饰 --> 大多数会忽略
- 动态绑定注意与静态绑定的互补关系以及调用时机
<只要是动态绑定生效之外,全部走的静态绑定的逻辑 --> (出乎意料的点)>
3. JNI如何实现数据传递?
---> 更具备实战性
考察点:
- 是否有Native开发经验(中级)
- 是否对JNI数据传递中的细节有认识(高级)
- 是否能够合理的设计JNI的界限(高级)
题目剖析:
关键词:数据传递
- 传递什么数据?
- 如何实现内存回收?
- 性能如何?
- 结合实例来分析更有效
Bitmap:Native层也有一个类对应
通过底层的指针来找对应Bitmap,在Java里是一个long,比如compress方法里都有用到
(step1:还不错,还是有经验的)
字符串操作 ---> 场景相·对多,会拿Native方法去加密解密
- GetStringUTFChars/ReleaseStringUTFChars
- const char* --> 比较像Java里的byte[]
- 拷贝出Modified-UTF-8的字节流 --> 很有可能会转到字节码(因为更难)
- \0编码成0xC080,不会影响C字符的结尾
- GetStringChars/ReleaseStringChars
- const jchar* --> 对应Java的char
- JNI函数自动处理字节序转换(后面有要处理的)
- GetStringUTFRegion/GetStringRegion
- 先在C层创建足够容量的空间
- 将字符串的某一部分复制到开辟的空间
- 针对性复制,少量读取时效率更优 --> 特别长的字符
- GetStringCritical/ReleaseStringCritical
- 调用对中间会停掉JVM GC
- 调用对之间不能有其他的JNI操作
- 调用对可嵌套
字符串操作的isCopy --> GetStringUTFRegion/GetStringRegion肯定要拷贝
- *isCopy == false的时候,Native层指针直接指向了Java虚拟机的字符串,会导致GC无法回收<组织了GC>(如果回收了就成了野指针)
- *isCopy == true的时候,复制了一份字符串到Native,Native层指针就指向这个复制来的字符串,虚拟机部分就可以自由垃圾回收了。
---> 取决于虚拟机实现,多数是倾向于拷贝,减少虚拟机部分的逻辑(GetStringCritical得到原地址的可能性更高)
(step2:了解到比较深,native访问字符串肯定没问题)
对象数组传递
- LocalReference使用的时候有个数限制,用完以后要主动释放(如果用的很少,函数调用完就释放了)
- Native里面不能调用Java反射,所以尽量不传Object,直接传基本类型
DirectBuffer
在物理层上开辟了一块内存,Java虚拟机和Native层都可以读写它
--> DirectBuffer里Native层需要自己处理字节序(GetStringChars方法,JNI函数里帮你处理了)
(step3:回答很有亮点、细节很到位,谁都知道可以用,减少复制,但是字节序这个东西用过才知道)
本节回顾:
- 通过long类型传递底层对象指针给Java层 --> 系统的Bitmap/线程安全的Thread
- 注意String的几组函数操作的区别与适用场景
- 注意对象数据较大时localRef超过上限的问题
以及边界设置问题,尽可能让底层尽可能少的接受Java对象,比如传基本类型,代价比较小
- 拷贝代价
- 访问代价
- 逻辑代价:让Native层关心效率,不关心逻辑 - 注意使用DirectBuffer时字节序的问题
---> 会引向字节码,会往虚拟机方向,就阻挡不了拿高分的步伐。
4. 如何全局捕获Native异常?
考察点:--> 得有思路
- 是否熟悉Linux的信号(中级)
- 是否熟悉Native层任意位置获取jclass的方法(高级)
- 是否熟悉底层线程与Java虚拟机的关系(高级)
- 通过实现细节的考察,确定候选人的项目经验(高级)
题目剖析:
关键词:全局捕获Native异常
- 如何捕获Native异常
- 如何清理Native层和Java层的资源
- 如何为问题的排查提供支持
捕获Native异常
- 抛异常都会有一个Signal,所以捕获下信号就好了
- 要把旧的保存起来,因为旧的有些系统的处理。某些不可恢复的情况下要把进程干掉,如果不让它调用不太好
--> 调用自己的android_signal_handler里面调用到系统的old_signalhandlers
(step1:对这块有了解) --> 不懂不懂
传递异常到Java层
自定义JNIEnvHelper
- 如果是Native线程,需要绑定到JVM才可以获得JNIEnv(AttachCurrentThread)
- 如果是Native线程,只有解绑时才会清理期间创建的JVM对象(如果不DttachCurrentThread,线程一直会绑定到Java虚拟机上,里面的引用一直没法被回收) --> 巧妙利用析构函数
(step2:想的比较多,犯的错误就会比较少) --> 必须要native经验了
<拿到类,进行反射,但拿类的过程会比较崎岖坎坷>
findClass:让Java层也有感知(回调到Java的细节)
---> 通常来说,拿类都是通过JNIEnv的findClass,但本质上是检查当前调用堆栈里面对应的Java的类,如果里面有,就直接拿他的ClassLoader然后去findClass
问题1:native创建的线程,需要Attach绑定到虚拟机上,从它的堆栈里连想要Java类都找不到,更不用说ClassLoader
问题2:Java层用的ClassLoader比较多,如何和触发signal调用的类不一样,也是找不到的
<所以要把想要调用的Java类的ClassLoader传进来>
---> 再细节:传入ClassLoader拿到的实例,是本地引用,出了调用就会被清空,需要NewGlobalRef来保存
(step3:知道细节非常多,面试官最喜欢的也是扣细节,基础很重要,一定要扎实,不能成为别人进攻你的漏洞)
捕获Native异常堆栈
- 设立备用栈,防止SIGSEGV因栈溢出而出现堆栈被破坏
- 创建独立线程专门用于堆栈收集并回调至Java层
- 收集堆栈信息
-[4.1.1,5.0]使用内置libcorkscrew.so
-[5.0,+∞]使用自己编译的libunwind - 通过线程关联Native异常对应的Java堆栈 --> 根据当前线程的名字来关联
(step3:有可能开发过这个框架,非常难得)
本节回顾:
- 通过捕获信号来捕获Native异常
- 使用JNIEnv的FindClass原理以及限制
---> 找当前Java调用堆栈的ClassLoader,如果这个ClassLoader load不出来你要的那个类就比较麻烦了。当前的JNIEnv实际上是通过AttachCurrentThread来绑定,当前线程肯定没有Java层的堆栈了,findClass就什么都没有了 - Native线程绑定到JVM及解绑到细节(对Java层的引用要都到detach的时候才去释放,一直不释放会GC不掉,导致内存泄露)
- Java对象的全局引用的应用(GlobalRef)
- Native层调用Java的方法 (反射)
---> 如果实现了,是个很好的框架,拿出来开源都没什么问题,好感倍增,不那么刁难 <必须去尝试一下>
5. 只有C、C++可以编写JNI的Native库吗?
考察点:
- 是否对JNI函数绑定的原理有深入认识(高级)
- 是否对底层开发有丰富的经验(高级)
---> 怎么绑定很熟了,才会往下问
题目剖析:
- Native程序与Java关联的本质上什么?
- 举例如何用其他语言编写符合JNI命名规则的符号
JNI对Native函数的要求
- 静态绑定
- 符号表可见
- 命名符合Java Native方法的包名类名方法名
- 符号名按照C语言的规则修饰
- 动态绑定
- 函数本身无要求
- JNI可识别入口函数如JNI_OnLoad进行注册即可
---> JNI_OnLoad实现要按照符号表可见、符号名按C语言的规则修饰
(step1:基础Ok)
可选的Native语言
- Golang
- Rust
- Kotlin Native(全平台)
- Scala Native
(step2:有全栈思维的工程师)
认识Kotlin native TODO:05:11
Kotlin Native编写JNI Native库 TODO:06:06
(JNI_OnLoad: TODO:09:13 --> 关键点还是findClass)
(step3:已经很拔高了,非常棒,超出预期)
本节回顾:
- 阐述JNI绑定方法的本质
- 介绍常见的C/C++以外的Native语言
- 以Kotlin Native为例实现JNI的Native库(说出注意点就行)
第6章 不屑一顾:居然问我 Activity 这么“简单”的东西?
1. Activity的启动流程上怎样的?
考察点:
- 是否熟悉Activity启动过程中与AMS的交互过程(高级)
- 是否熟悉Binder通信机制(高级)
- 是否了解插件化框架如何Hook Activity启动(高级)
- 阐述Activity转场动画的实现原理可加分(中级)
- 阐述Activity的窗口显示流程可加分(高级)
题目剖析:
关键词:Activity、启动流程
- 与AMS如何交互
- Activity的参数和结果如何传递
- Activity如何实例化
- Activity生命周期如何流转
- Activity的窗口如何显示
- Acyivity转场动画的实现机制
Activity跨进程启动 TODO 01:00 - 01:18 + 03:16(红圈里的是重点,有binder交互)
(step1:基本可以了)
Activity进程内启动 TODO:04:06(红点部分是插件化用来Hook的)
(step2:非常大亮点了,如果还能把插件化框架答得很好,已经接近满分了)
Activity的参数传递
- 通过Binder缓冲区传递
(Binder缓冲区:1.大小有限制;2.数据必须可以序列化)
---> 也是Google官方人员为了避免Activity携带太大数据导致太重苦心设计的 - 如何突破Binder缓冲区的大小限制:
- 分离View和Model,Model可存内存或持久化
---> 架构设计(会的人太多也不算亮点了) - 跨进程需实现进程通信机制(比如播放器,播放页面可以放在独立进程里)
Activity实例化
Instrumentation里,通过一个Factory把ClassLoader和className传进去,通过反射创建
--> 因为是通过反射(newInstance)创建,所以Activity必须有个无参的构造方法,所以Activity不能自己添加构造器
---> Fragment最好也不要添加有参数的构造方法
Activity状态如何保存和恢复
销毁或者说保存的时候会涉及FragmentManager,会把Activity里面持有的Fragment保存到以android:fragments为key的数据里,保存了现在显示的fragment、顺序以及位置等信息,在恢复的时候也是通过实例化Activity一样的反射实现
---> 毕竟它又不知道哪些参数,不知道怎么还原
Activity的窗口如何展示 TODO:13:40
1 实例化Activity以后,进入activity-attach,执行createPhoneWindow,创建Window
- 进入activity-create执行installDecor,创建DecorView,包括status bar、action bar、content view
- 进入activity-resume之后马上makeVisible
(所以在resume之前是还没显示的,因为窗口还美显示)
(step3:攒够了加分项)
Activity转场动画的实现机制
---> 原理简单:附加分
在播放动画或实现页面跳转之前,把需要实现转场的元素拿到,记录位置和大小等信息,在新的页面显示之前把这些共享元素的信息拿到把它应用到新的页面的共享元素所在的位置,执行动画。
(View、Fragment里都可使用)
本节回顾
- Activity启动流程是一个庞大的题目,足够灵活
- 启动过程中涉及到与AMS的交互与插件化紧密相关
- 参数传递机制可与架构设计联系进行迁移
- 生命周期与窗口展示可以向事件处理、UI绘制等话题迁移
技巧点拨
- 知识迁移
- 任何Activity相关的问题都可以迁移到复杂的UI展示上
- 任何Activity相关的问题都可以迁移到插件化上
2. 如何跨App启动Activity?有哪些注意事项?
考察点:
- 是否了解如何启动外部应用的Activity(初级)
- 是否了解如何防止自己的Activity被外部非正当启动(中级)
- 是否对拒绝服务漏洞有了解(高级)
- 如何在开发时规避拒绝服务漏洞(高级) --> 也要关注第三方是否有
题目剖析:
关键词:跨App、注意事项
共享uid的App
系统应用/全家桶比较多
manifest里配置一样的android:sharedUserId
就可以直接启动对方的Activity
(而且共享流量的计算 --> Linux本身的流量计算机制)
Ex:kotlin是加分项,但是了解不行(kotlin的类型可控怎么实现,怎么和Java互通)
使用exported
activity设置exported以后,相当于把Activity整个暴露在系统当中,随便一个应用都可以访问你。
使用intentFilter
比如设置action,外部就可以通过action访问到
(step1:知道怎么用了 --> 非常基础,可以往下问了,不问的话要自己说)
为允许外部启动的Activity加权限
在目标应用的App B的manifest声明自定义的权限,activity里面引用,那么调用者App A就必须声明这个权限才能调用(相当于App B里的Activity被加锁了)
---> 需要注意的是,App B必须比App A先安装,App A才能获取到权限
(step2:至少有安全意识的)
拒绝服务漏洞
App A 在启动App B的Activity的时候往Bundle里面扔了一个SerializableA,App B里没有这个类,但是App B只要访问了Intent的extra,一定会反序列化,就会抛找不到类的异常,也就是拒绝服务漏洞。
--> 静态分析工具会这么做。
(step3:遇到过这个问题,有实战经验的)
解决:try...catch即可
--> 自己的app好加,但是如果是sdk里面拦截了生命周期(ActivityLifecleCallbacks),访问了intent的extra也会有拒绝服务漏洞(例如:小米Push v3.9.6)
(最根本的不要随便把activity暴露给别人,所以AS里设置exported为true的时候会提示不安全的)
本节回顾:--> 和启动流程一脉相承,可以顺便答了
- 跨App启动Activity 首先要明确App之间的关系(sharedUid就好办很多了)
- 外部可启动expored或有intentFilter的Activity
- 可外部启动的Activity需要注意拒绝服务漏洞
- 尽量不暴露Activity,为暴露的Activity加权限控制
- Intent的Extras读取时要注意捕获异常
3. 如何解决Activity参数的类型安全及接口繁琐的问题?
---> 前面提过onActivityResult问题
考察点:
- 是否有代码优化和代码重构的意识(高级)
(有没有觉得启动Activity的地方非常危险?写起来一堆静态常量作为key也是挺恶心的,特别是Activity很多的情况) - 是否对反射、注解处理器有了解(高级)
- 是否具备一定的框架设计能力(高级)
题目剖析:
关键词:类型安全、接口繁琐
- 类型安全:Bundle的K-V不能在编译期保证类型(不利于协作开发)
- 接口繁琐:启动Activity时参数和结果传递都依赖Intent(很多get、get、get、put、put、put)
- 等价的问法:设计一个框架,解决上述问题(ActivityStarter、铁观音)
- 面试不需要实现,只管合理大胆的想
回顾:onActivityResult为什么麻烦?
- 代码处理逻辑分离,容易出现遗漏和不一致的问题
- 写法不够直观,且结果数据没有类型安全保障
- 结果种类较多时,onActivityResult就会逐渐臃肿难以维护
为什么Activity的参数存在类型安全问题?
因为Intent的put和get分离,且无强相关,参数类型安全需要人工保证
--> 人工容易遗忘,且项目变大以后很难保证
(step1:意识到了这里面有问题)
调起方:通过注解处理器生成Builder,区分必须的和可选的,且类型强绑定
被调起方:注解处理器生成注入逻辑(注意拒绝服务漏洞)
注入逻辑调用时机:
- 在onCreate里注入(拦截ActivityLifecycleCallback的onActivityCreated)
- onNewIntent没有对应的生命周期回调:需要开发者调用静态方法
- updateIntent:通常onNewIntent穿进来的Intent,不会保存到Activity.getIntent()的那个Intent里,需要更新的时候需要调用Activity.setIntent()
支持结果返回:注意更新Activity实例(和前面onActivityResult一样的问题)
支持更多类型:提供UserConverter,比如User,可以把字段一个一个的put到bundle,然后读的时候组装回来
支持更多功能:转场动画、SharedElement、categories、flags、返回结果、mode(支持kotlin)
支持Fragment:没有Activity那么多的配置,传参的时候用Arguments,也是和bundle一样的问题
Optional提供默认值:优化部分
注解处理器程序的基本思路: --> 生成代码
读取注解信息 --> 构造生成的类结构
注解处理器的开发注意事项
- 注意注解标注的类的继承关系 --> 可能要把父类的标注合并过来
- 注意注解标注的类为内部类的情况(用的api不同,编译生成的名称有$)
- 注意Kotlin与Java的类型映射的问题(现在注解编译器插件不支持直接编译kotlin是要转成Java类型的 --> 毕竟相关库不多)
- 把握好代码生成和直接依赖的边界(尽量少生成代码、编译时间会比较长) --> 非模版的代码可以写成依赖库
(step2:注解处理器开发了解的很多了 /)
元编程 TODO:18:27
用代码来写代码
阐述过程:先APT(dagger、ARouter),再优先讲泛型、再反射、再动态代理代理(retrofit)、再字节码(Dagger)
(泛型是介于编译期和运行期之间的)
(step3:非常高端了,满分,远远甩开别人)
本节回顾:
- 分析Activity参数传递存在的问题
- 设计一个基于注解处理器简化参数传递的框架
- 简单介绍了元编程相关的概念
---> 证明自己的能力,就要争第一
4. 如何在代码的任意位置为当前的Activity添加View
考察点:
- 如何在任意位置获取当前Activity(中级)
- 是否对Activity的窗口有深入认识(高级)
- 潜在的内存泄露风险以及内存回收机制(高级)
- 是否能够深入需求评估技术方案的合理性(高级)
题目剖析:
关键词:任意位置、添加View
- 如何获取当前Activity?
- 如何在不影响正常View展示的情况下添加View?
- 既然能添加,就应当能移除,如何移除? --> 没有removeContentView
- 这样做的目的是什么?添加全局View是否更合适?
获取当前Activity
ActivityLifecycleCallbacks,小心内存泄露,用WeakReference修饰CurrentActivity
---> 很可能就引入了垃圾回收
内存回收机制
没有使用引用计数,而是GC Roots。
GC Root:
- 虚拟机栈帧 --> 方法没结束的时候
- 类静态属性引用的对象
- 常量引用的对象
- Native方法引用的对象
软引用和弱引用的区别,内存足够的情况下,不回收软引用
(step1:垃圾回收机制答出来并且能发散)
添加View
有addContentView,没有removeContentView,其实都是在DecorView里,content view有个全局唯一的id:android:id/content,所以可以很方便的find到,来add或remove
---> 注意使用时机:如果DecorView还没有,findViewById为空
(step2:对View的展示已经了如指掌,已经是个满意的答案)
---> 突出亮点,技术上已经就这样了,思考合理性
添加全局View
(题目需求类似iPhone的那个点,那通过Window来实现是否更合适)
明确需求选择合适的技术方案
(step3:作为高级工程师需要考虑合理性,不能让下面的人乱搞,产品提过来的时候也要考虑)
本节回顾:
- 通过Activity生命周期回调获取当前Activity
- 分析几种引用在内存垃圾回收时的不同表现
--> 尽可能避免小对象、浮动对象、触发young GC,老年代的GC等,内存回收Android上和Java上的HotSpot有什么区别(都是亮点) - 分析如何实现Activity添加和移除contentView(拿到父View去移除,当然getParent去移除也可以)
- 评估需求,确认使用场景给出更合理的解决方案
5. 如何实现类似微信右滑返回的效果
考察点:
- 是否熟练掌握手势和动画的运用(中级)
- 是否了解窗口绘制的内部原理(高级)
- 是否对Activity的窗口有深入认识(高级)
题目剖析:
关键词:微信右滑返回 --> 类似iOS的Navigation效果
- 没有明说UI的类型,Activity还是Fragment?
- Fragment实现简单,重点回答Activity的实现
- 考虑如何设计这样一个组件
- 考虑如何降低接入成本
Fragment的实现
- 对于Fragment的控制相对简单(不涉及window、堆栈)
- 不涉及window的控制,只是View级别的操作
- 实现View跟随手势滑动的效果(translation、offset)
- 实现手势结束后判断取消或返回执行归位动画
(step1:有基本的认识)
Activity的Window
滑的过程中要看到后面Activity,需要把前面的Activity设置为透明的(windowsIsTranslucent和windowBackgroud都需要设置)
--> 如果不设置为透明的,后面的没显示,系统以为前面的为实心的,为了节省内存就不绘制后面的了(为黑色的)。
当前Activity画布不透明,前一个Activity没绘制
Activity联动-多Task
A、C在Task0,B在Task1,按A->B->C切换,在B->C切换的过程中,先瞬间切换到Task0,显示了A,然后再显示了C --> 实践一下(做这个侧滑返回的概念就更会这样了)
解决:开始播放动画的时候,先给B拍张照片,放到C下面
--> Activity栈获取:通过getTaskId获取它到底在拿个栈里面
Activity透明对生命周期的影响 TODO:08:01
- 看到一个Activity至少在started,如果弹个对话框,activity进入onPause,也是在started状态
- 如果最上面的那个activity是透明的话,它下面的就处于started状态了(如果它下面的也透明,一直往下判断知道遇到一个不透明的)
---> 原因:Android系统这样设计是为了性能,前面不透明的话,把后面画出来就浪费了
(step2:对窗口显示逻辑理解很深了)
设计SDK-现有方案 ---> 基于SwipeBackLayout
- 继承子SwipeBackActivity:完成了主要的手势和动画实现
---> 尽量不要影响用户的继承结构 - 保证Activity窗口透明(会造成额外开销,下面的activity永远都会被绘制)
---> 最好不要直接影响主题,动画时让窗口透明即可
解决: - 用组合替代继承,把功能移到外部类当中
- 动态替换切换窗口透明状态:反射调用隐藏函数convertToTranslucent/convertFromTranslucent(需要页面滑动的时候变成透明的,滑动结束后变回来)
---> 实现:调用了AMS的convertToTranslucent,通知栈中的Activity恢复可见,生命周期的变化在此处理
(step3:关键不是api调用,而是想到怎么优化sdk,减少接入成本和性能优化,满分回答 /)
本节回顾:
- 分许需要背景以及不同场景的解决方案
- 分析Activity窗口透明机制以及对生命周期的影响
- 分析SwipeBackLayout存在的接口设计问题
- 给出优化的接口设计方案
技巧点拨:
- 造轮子
- 把你的轮子放GitHub上接受挑战
- 改进现有的轮子
- 挑战别人的轮子,促进开源和自己能力的提升
网友评论