Android面试题集锦--上
站长
· 阅读数 3
1. Java基础相关笔记
- Java的char是两个字节,如何存utf-8字符?
面试官视角:这道题想考察什么? 1. 是否熟悉Java char和字符串(初级) 2. 是否了解字符的映射和存储细节(中级) 3. 是否能触类旁通,横向对比其它语言(高级) 题目结论: 初级:Java char不存utf-8的字节,而是utf-16,占两个字节;utf-8是1-6个字节存储;Unicode字符集(码点,人类认知层面的字符映射成整数,字符集不是编码) 中级:Java char中存储的是utf-16编码。Unicode通用字符集占两个字节,'中',占一个char两个字节,byte[] bytes = '中'.getBytes("utf-16")--> 结果为:fe ff 4e 2d(fe ff为字节序) 高级: 1. 令人迷惑的字符串长度:字符串长度 != 字符数(比如:表情emoji,一个字符数,占两个长度) 2. 触类旁通: 1). Java9中对拉丁(Latin)字符优化:例如若字符串中是ascii码字符,只需要7个byte一个字节表示存储,而使用utf-16存储明显是有压力的。Java9若发现整个字符串中只有ascii码字符,则会使用byte来存,不使用char存储,这样就会节省一半字符,此时字符串长度 也!= 字符数。 2). Java(String emoji = "emoji表情",长度为2) == Python(>=3.3,emoji = u"emoji表情",长度为1)
- Java String可以有多长?
面试官视角:这道题想考察什么? 1. 字符串有多长是指字符数还是字节数(初级) 2. 字符串有几种存在形式(中级) 3. 字符串的不同形式受到何种限制(高级) 题目结论: 初级:分为两种形式写在代码中的字面量和文件中的字符串,即存在于栈内存中和堆内存中 中级: 栈内存中:字面量中,受限于Java代码编译完成的字节码文件CONSTANT_Utf8_info结构中的u2 length。u2代表两个字节,即长度最大为16位二进制表示,因此最大长度为65535。CONSTANT_Utf8_info运行时,会被加载到Java虚拟机方法区的常量池中,若常量池很小,字符串太大,会出现异常,常量池一般不会连65535都无法存储。 高级: 1. Java编译器的bug:实战过程中发现输入65535个Latin字符,编译无法通过,而65534则可以。Java不会和c语言中一样,在字符串中加一个'\0'。Java编译器(Javac)中判断字符串长度时使用的是 <65535 号,而非 <=65535,因此65535无法编译通过。kotlin中是没有问题的 2. String中若为中文字符,比如String longString = "烫烫烫...烫烫烫"; 烫占3个字节,理论上最长为 65535/3 个。实战输入65535/3个烫,发现是可以编译通过的,原因:Java编译器对于中文字符这样,需要Utf8编码这种,没办法在编译时直接知道到底需要占用多少字节,只能先通过utf-8编码,然后再来查看占多少字节。此写法判断长度时使用 >65535 ,因此此处是正确的。 3. 总结:Latin字符,受Javac限制,最多65534个;非Latin字符最终对应字节个数差距较大,最多字节数为65535个;若运行时方法区设置较小(比如:嵌入式设备),也会受方法区大小限制。 4. 堆内存中:new String(bytes); 受String内部value[] 数组,此数组受虚拟机指令 newarray [int],因此数组理论上最大个数为 Integer.MAX_VALUE。有一些虚拟机可能保留一下头信息,因此实际上最大个数小于 Integer.MAX_VALUE。即堆中String理论上最长为Integer.MAX_VALUE,实际受虚拟机限制小于Integer.MAX_VALUE,并且若堆内存较小也受堆内存大小限制。
- Java匿名内部类有哪些限制?
面试官视角:这道题想考察什么? 1. 考察匿名内部类的概念和用法(初级) 2. 考察语言规范以及语言的横向对比等(中级) 3. 作为考察内存泄漏的切入点(高级) 题目结论: 初级:没有人类认知意义上的名字;匿名内部类的实际名字:包名+外部类+$N,N是匿名内部类的顺序(即在外部类中所有匿名内部类从上到下排第几) 中级: 匿名内部类的继承结构:匿名内部类必然有其父类或者父接口,在初始化匿名内部类new InnerClass(){ ... },InnerClass类实际上就是此匿名内部类的父类 匿名内部类只能继承一个父类或者实现一个接口,不能同时即继承父类由实现接口;kotlin中是可以的 高级: 匿名内部类的构造方法:编译器生成,匿名内部类可能会有外部类的引用,可能会导致内存泄漏。 匿名内部类实际参数列表: 1. 外部类实例(定义在非静态域内) 2. 父类的外部对象(父类非静态,即父类是定义在一个类中,外部对象是指父类的外部类实例) 3. 父类的构造方法参数(父类若由构造方法且参数列表不为空) 4. 外部捕获的变量(方法体内有引用外部的final变量) 匿名内部类参数列表所知: 1. 父类定义在是非静态作用域内(即父类是定义在一个类中),会引用父类的外部类实例 2. 如果定义在非静态作用域内(非静态方法内),会引用外部类实例(非静态方法内所在类的实例) 3. 只能捕获外部作用域的final变量 4. Java8:创建时只有单一方法的接口可以用Lambda转换(SAM类型),只能是接口且接口中只有一个方法
- Java 方法分派
概念引入1:Java的虚方法(需要注意虚方法和抽象方法并不是同一个概念) 虚方法出现在Java的多态特性中,父类与子类之间的多态性,对父类的函数进行重新定义。 Java虚方法你可以理解为java里所有被overriding的方法都是virtual的,所有重写(覆写)的方法都是override的。 在JVM字节码执行引擎中,方法调用会使用invokevirtual字节码指令来调用所有的虚方法。 概念引入2:方法调用 java是一种半编译半解释型语言,也就是class文件会被解释成机器码,而方法调用也会被解释成具体的方法调用指令,大致可以分为以下五类指令: invokestatic:调用静态方法; invokespecial:调用实例构造方法,私有方法和父类方法; invokevirtual:调用虚方法(普通实例方法调用是在运行时根据对象类型进行分派的,相当于C++中所说的虚方法); invokeinterface:调用接口方法,在运行时再确定一个实现此接口的对象; invokedynamic:在运行时动态解析出调用点限定符所引用的方法之后,调用该方法; 注意:invokedynamic 指令是jdk1.7才加入的,但是在jdk1.7中并没有开始使用。在jdk1.8中才开始大量使用,主要就是我们大量用的 lambda 表达式。 方法绑定: 静态绑定:如果在编译时期解析,那么指令指向的方法就是静态绑定,也就是private,final,static和构造方法,也就是上面的invokestatic和invokespecial指令,这些在编译器已经确定具体指向的方法。 动态绑定:而接口和虚方法调用无法找到真正需要调用的方法,因为它可能是定义在子类中的方法,所以这种在运行时期才能明确类型的方法我们成为动态绑定。 C++中,如果不将函数xxx声明为virtual,那么无论子类是否自己定义了和父类同名xxx的方法,父类Base中调用的xxx方法永远都是父类中的(静态绑定)。 当为xxx函数添加virtual声明,使其成为一个虚函数时,此时Base类会产生和维护一个虚函数表。同理,派生的子类Sub也会有一个虚函数表,对虚函数的调用都是动态绑定的,与JAVA原理类似,都是使用虚函数在虚函数表中的索引偏移量来取得函数的实际地址。 概念引入3:虚分派(虚方法的分派) 概念2中5种方法调用指令最复杂的要属 invokevirtual 指令,它涉及到了多态的特性,使用 virtual dispatch 做方法调用。 virtual dispatch 机制会首先从 receiver(被调用方法的真实对象)的类的实现中查找对应的方法,如果没找到,则去父类查找,直到找到函数并实现调用,而不是依赖于引用的类型。 面试官视角:这道题想考察什么? 1. 多态、虚方法表的认识(初级) 2. 对编译和运行时的理解和认识(中级) 3. 对Java语言规范和运行机制的深入认识(高级) 4. 横向对比各类语言的能力(高级) 1. Groovy,Gradle DSL 5.0以前唯一正式语言 2. C++,Native程序开发必备 题目剖析:怎样理解Java 的方法分派? 1. 就是确定调用谁的、哪个方法 2. 针对方法重载的情况进行分析 3. 针对方法覆写的情况进行分析 题目示例: SuperClass super = new SubClass(); printHello(super); public static void printHello(SuperClass super) { System.out.println("Hello " + super.getName()); } public static void printHello(SubClass sub) { System.out.println("Hello " + sub.getName()); } 题目结论: 初级:方法的输出?输出的子类重写的方法,取决于运行时的实际类型 中级:方法的调用?Java调用编译时期声明的类型,即:printHello(SuperClass super) 高级: Java方法分派: 静态分派(方法重载分派):编译器确定;依据调用者的声明类型和方法参数类型 动态分派(方法重写分派):运行时确定;依据调用者的实际类型分派 Groovy语言:方法输出同Java;方法调用根据实际类型调用,即:printHello(SubClass sub) C++语言: C++题目示例: // 方法输出 SuperClass super = SubClass(); // 分配于栈内存中 cout << super.getName() <<endl; SuperClass *superClass = new SubClass(); // 分配于堆内存中 cout << superClass->getName() <<endl; delete(superClass); // 方法调用 void printHello(SuperClass super) { cout << super.getName() <<endl; } void printHello(SubClass sub) { cout << sub.getName() <<endl; } void printHello(SuperClass* superClass) { cout << superClass.getName() <<endl; } void printHello(SubClass* subClass) { cout << subClass.getName() <<endl; } printHello(super); // 1 printHello(*superClass); // 2 printHello(&super); // 3 printHello(superClass); // 4 1. 方法的输出: 若父类函数没有添加virtual声明,即没有声明为虚方法,则C++中均使用父类方法 若父类函数添加virtual了声明,则又分两种情况: 1. 栈内存中(直接声明):由于多余部分被舍弃,因此输出的还是父类方法 2. 堆内存中(new出来的):堆内存中,同Java,因此同Java,输出的是子类的方法 注:C++中对象new出来和直接声明是有区别的,new出来的对象是直接使用堆空间,而直接声明(局部)一个对象是放在栈中。 new出来的对象的生命周期是具有全局性,譬如在一个函数块里new一个对象,可以将该对象的指针返回回去,该对象依旧存在,因此需要delete销毁。new对象指针用途广泛,比如作为函数返回值、函数参数等。 直接声明是根据声明的类型分配内存的,即SuperClass super = SubClass(); 是根据SuperClass分配内存的。而SubClass占用内存>SuperClass占用内存,因此若直接声明这种,会将SubClass中多余的部分去掉,即相当于分配的就是SuperClass对象。 2. 方法的调用(getName已是虚方法): 1. 栈内存中(直接声明): 1. printHello 1 :调用的printHello(SuperClass super),由于已经被剪裁(舍弃) 过啦,输出的是父类方法 2. printHello 3 :由于传入&super,即super的地址(即指针),而super类型为SuperClass,因此指针类型也是SuperClass,最终调用printHello(SuperClass* superClass)。由于已经被剪裁(舍弃) 过啦,输出的是父类方法。 2. 堆内存中(new出来的): 1. printHello 2 :由于*superClass 已经将指针指向的值取出来了,而且指针类型为SuperClass,所以此值也被剪裁过啦,最终调用的printHello(SuperClass super)。 2. printHello 4 :传入一个SuperClass指针,因此调用的是printHello(SuperClass* superClass)。而superClass指针指向的真实对象是SubClass,而getName前提已经是虚方法,因此最终输出的是子类SubClass的getName方法。
- Java 泛型的实现机制是怎样的?
概念引入1:方法签名(在Java的世界中, 方法名称+参数列表,就是方法签名) 1. 在Java中,函数签名包括函数名,参数的数量、类型和顺序。关于方法签名定义的注意点:方法签名中的定义中不包括方法的返回值、方法的访问权限。 2. 谈到方法签名,就要谈谈重写,Override,重写方法,就是方法签名完全一致的情况,子类的方法签名必须与父类的一模一样。 3. Overload方法重载(overloading) 是在一个类里面,方法名字相同,而参数不同。 4. 获取方法签名: javap -s 包名.类名 概念引入2:类型擦除 泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。 原始类型(类型擦除后保留的原始类型):就是擦除了泛型信息,最后在字节码中的类型变量的真正类型。在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T> 则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String> 则类型参数就被替换成类型上限String。 类型擦除示例: List<String> ll = new ArrayList<>(); List<Integer> kk = new ArrayList<>(); System.out.println(ll.getClass());//输出:class java.util.ArrayList System.out.println(kk.getClass());//输出:class java.util.ArrayList System.out.println(ll.getClass() == kk.getClass());//输出:true 类型擦除引起的问题及解决方法: 1. 先检查,再编译以及编译的对象和引用传递问题 一问:既然说类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList<String> 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? 一答:Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,最后进行编译。 二问:这个类型检查是针对谁的呢?我们先看看参数化类型和原始类型的兼容(以 ArrayList举例子): 以前的写法:ArrayList list = new ArrayList(); 现在的写法:ArrayList<String> list = new ArrayList<String>(); 如果是与以前的代码兼容,各种引用传值之间,必然会出现如下的情况: ArrayList<String> list1 = new ArrayList(); //第一种情况 ArrayList list2 = new ArrayList<String>(); //第二种情况 这样是没有错误的,不过会有个编译时警告。不过在第一种情况,可以实现与完全使用泛型参数一样的效果,第二种则没有效果。 因为类型检查就是编译时完成的,new ArrayList()只是在内存中开辟了一个存储空间,可以存储任何类型对象,而真正设计类型检查的是它的引用,因为我们是使用它引用list1来调用它的方法,比如说调用add方法,所以list1引用能完成泛型类型的检查。而引用list2没有使用泛型,所以不行。 二答:类型检查针对的是引用,因此ArrayList<T>类型检测是根据引用来决定的。即ArrayList内存存储类型,是通过引用的<T>来决定 三问:泛型的引用传递问题(以 ArrayList举例子): ArrayList<String> list1 = new ArrayList<Object>(); //第一种,编译错误 相当于: ArrayList<Object> list1 = new ArrayList<Object>(); list1.add(new Object()); list1.add(new Object()); ArrayList<String> list2 = list1; //编译错误 ArrayList<Object> list2 = new ArrayList<String>(); //第二种,编译错误 相当于: ArrayList<String> list1 = new ArrayList<String>(); list1.add(new String()); list1.add(new String()); ArrayList<Object> list2 = list1; //编译错误 三答: 第一种:我们先假设它编译没错。那么当我们使用list2引用用get()方法取值的时候,返回的都是String类型的对象(上面提到了,类型检测是根据引用来决定的),可是它里面实际上已经被我们存放了Object类型的对象,这样就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷)。 第二种:这样的情况比第一种情况好的多,最起码,在我们用list2取值的时候不会出现ClassCastException,因为是从String转换为Object。可是,这样做有什么意义呢,泛型出现的原因,就是为了解决类型转换的问题。我们使用了泛型,到头来,还是要自己强转,违背了泛型设计的初衷。所以java不允许这么干。再说,你如果又用list2往里面add()新的对象,那么到时候取得时候,我怎么知道我取出来的到底是String类型的,还是Object类型的呢? 2. 自动类型转换 问:因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢? 答:ArrayList.get()方法中 return (E) elementData[index]; 可以看到,在return之前,会根据泛型变量进行强转。假设泛型类型变量为Date,虽然泛型信息会被擦除掉,但是会将(E) elementData[index],编译为(Date)elementData[index]。所以我们不用自己进行强转。当存取一个泛型域时也会自动插入强制类型转换。 3. 泛型类型变量不能是基本数据类型:不能用类型参数替换基本类型。就比如,没有ArrayList<double>,只有ArrayList<Double>。因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。 4. 运行时类型查询:因为类型擦除之后,ArrayList<String>只剩下原始类型,泛型信息String不存在了。那么,运行时进行类型查询( if( arrayList instanceof ArrayList<String>) )的时候是错误的。 5. 泛型在静态方法和静态类中的问题 1. 泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数。因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。 // 示例: public class Test2<T> { public static T one; //编译错误 public static T show(T one){ //编译错误 return null; } } 2. 注意区分下面的一种情况:因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的 T,而不是泛型类中的T。 public class Test2<T> { public static <T >T show(T one){ //这是正确的 return null; } } 6. 类型擦除与多态的冲突和解决方法 解决方法:桥方法 具体参考:https://www.cnblogs.com/wuqinglong/p/9456193.html 中的3.3节 类型擦除优点: 1. 运行时内存负担小 2. 兼容性好 类型擦除带来的局限性:类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。 泛型中值得注意的地方: 1. 泛型类或者泛型方法中,不接受 8 种基本数据类型。 2. 对泛型方法的困惑:public <T> T test(T t){ return null; } 连续的两个 T,其实 <T> 是为了说明类型参数,是声明,而后面的不带尖括号的 T 是方法的返回值类型。 3. Java 不能创建具体类型的泛型数组:例如: List<Integer>[] li1 = new ArrayList<Integer>[]; 和 List<Boolean> li2 = new ArrayList<Boolean>[]; 无法在编译器中编译通过的。原因还是类型擦除带来的影响。 List<Integer> 和 List<Boolean> 在 Jvm 中等同于List<Object> ,所有的类型信息都被擦除,程序也无法分辨一个数组中的元素类型具体是 List<Integer>类型还是 List<Boolean> 类型。 解决方式:使用通配符 List<?>[] li3 = new ArrayList<?>[10]; li3[1] = new ArrayList<String>(); List<?> v = li3[1]; 借助于无限定通配符却可以,?代表未知类型,所以它涉及的操作都基本上与类型无关,因此 Jvm 不需要针对它对类型作判断,因此它能编译通过,但是,只提供了数组中的元素因为通配符原因,它只能读,不能写。比如,上面的 v 这个局部变量,它只能进行 get() 操作,不能进行 add() 操作。 通配符 ?:除了用 <T> 表示泛型外,还有 <?> 这种形式。? 被称为通配符。通配符的出现是为了指定泛型中的类型范围。 示例:class Base{} class Sub extends Base{} 说明:Base 是 Sub 的父类,它们之间是继承关系,所以 Sub 的实例可以给一个 Base 引用赋值 List<Sub> lsub = new ArrayList<>(); List<Base> lbase = lsub; 说明:编译器不会让它通过的。Sub 是 Base 的子类,不代表 List<Sub> 和 List<Base> 有继承关系。 但是,在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。 通配符有 3 种形式: 1. <?> 被称作无限定的通配符。 2. <? extends T> 被称作有上限的通配符。 3. <? super T> 被称作有下限的通配符。 通配符的未知性: public void testWildCards(Collection<?> collection){} testWildCards方法内的参数是被无限定通配符修饰的 Collection 对象,它隐略地表达了一个意图或者可以说是限定,那就是 testWidlCards() 这个方法内部无需关注 Collection 中的真实类型,因为它是未知的。所以,你只能调用 Collection 中与类型无关的方法。即当 <?> 存在时,Collection 对象丧失了 add() 方法的功能,编译器不通过。 面试官视角:这道题想考察什么? 1. 对Java泛型使用是否仅停留在集合框架的使用(初级) 2. 对泛型的实现机制的认知和理解(中级) 3. 是否有足够的项目开发实战和"踩坑"经验(中级) 4. 对泛型(或模板)编程是否有深入的对比研究(高级) 5. 对常见的框架原理是否有过深入剖析(高级) 题目剖析: 1. 题目区分度非常大 2. 回答需要提及一下几点才能显得有亮点: 1. 类型擦除从编译角度的细节 2. 类型擦除对运行时的影响 3. 类型擦除对反射的影响 4. 对比类型不擦除的语言 5. 为什么Java选择类型擦除 3. 可从类型擦除的优劣着手分析回答 题目结论: 初级:泛型的类型擦除,可以缓解运行时内存压力(方法区只要保留一份List即可)以及其拥有更好的兼容性(1.5以前List就是List,ArrayList就是ArrayList,两者无关联) 中级: 泛型的缺陷1:基本类型无法作为泛型参数,因此基本类型为参就多了装箱和拆箱的过程。Google为规避基本类型的装箱和拆箱,自己为Android定制的SparseArray 泛型的缺陷2:泛型类型无法用作方法重载(因为类型擦除后,类型一致) public void printList(List<Integer> list){} public void printList(List<String> list){} 泛型的缺陷3:泛型类型无法当做真实类型使用(包括:运行时类型查询) if( arrayList instanceof ArrayList<String>){} // 非法类型判断,将<String>去掉则可以 知识迁移:Gson.fromJson(String json,Class<T> class) 为什么需要传入Class?需要根据参数传入的Class类型,最终强转后返回。即自动类型转换,需要告诉框架转换的类型 泛型的缺陷4:静态方法无法引用类泛型参数 泛型的缺陷5:类型强转的运行时开销,例如:ArrayList.get 高级: 附加的泛型类型签名信息(即泛型的真实类型,特定场景可通过反射获取): class SuperClass<T> {} class SubClass extends SuperClass<String> { public List<Map<String, Integer>> getValue() { return null; } } ParameterizedType parameterizedType = (ParameterizedType) SubClass.class.getGenericSuperclass(); // 获取方法返回值真实的泛型实参类型 ParameterizedType parameterizedTypeMethod = (ParameterizedType) SubClass.class.getMethod("getValue").getGenericReturnType(); Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); for (Type actualTypeArgument : actualTypeArguments) { System.out.println(actualTypeArgument); } // 输出:class java.lang.String 获取签名信息总结: 1. 如果是继承基类而来的泛型,就用 getGenericSuperclass() , 转型为 ParameterizedType 来获得实际类型 2. 如果是实现接口而来的泛型,就用 getGenericInterfaces() , 针对其中的元素转型为 ParameterizedType 来获得实际类型 3. 我们所说的 Java 泛型在字节码中会被擦除,并不总是擦除为 Object 类型,而是擦除到上限类型 4. 能否获得想要的类型可以在 IDE 中,或用 javap -v <your_class> 来查看泛型签名来找到线索 注意:混淆时要保留签名信息(Proguard文件中添加 -keepattributes Signature) 迁移:使用泛型签名的两个示例 1. Gson: Type collectionType = new TypeToken<Collection<Integer>>(){}.getType(); Collection<Integer> ints = gson.fromJson(json, collectionType); 2. Retrofit: @GET("users/{login}") Call<User> getUserCallBack(@Path("login")String login); 迁移:kotlin 反射的实现原理 使用Metadata注解,若使用kotlin的反射,并且需要混淆时,则注意:Proguard文件中添加(-keep class kotlin.Metadata{*;})
- Android的onActivityResult使用起来非常麻烦,为什么不设计成回调?
面试官视角:这道题想考察什么? 1. 是否熟悉onActivityResult的用法(初级) 2. 是否思考过用回调代替onActivityResult(中级) 3. 是否实践过用回调代替onActivityResult(中级) 4. 是否意识到回调的问题(高级) 5. 是否能给出匿名内部类对外部引用的解决方案(高级) 题目剖析: 1. onActivityResult为什么麻烦?(初级) 2. 为什么不使用回调?(中级) 题目结论: 初级:1). 代码处理逻辑分离,容易出现遗漏和不一致问题 2). 写法不够直观,且结果数据没有类型安全保障 3). 结果种类较多时,onActivityResult就会逐渐臃肿难以维护 第一步:ActivityA ————> ActivityB startActivtityResult(intent, requestCode); 代码处理逻辑分离,容易出现遗漏和不一致问题 第二步:ActivityA <———— ActivityB setResult(resultCode, intent); 写法不够直观,且结果数据(Bundle)没有类型安全保障 第三步:ActivityA @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {} 结果种类较多时,onActivityResult就会逐渐臃肿难以维护 中级: 假设尝试使用回调startActivtityResult(intent, new onResultCallBack() { ... });,第一步和第三步能够解决,ActivityB finish后回调ActivityA。能解决1和3步也不错,那为什么不使用回调呢? 总有例外状况,若ActivityB长时间处于前台,而ActivityA由于内存或者策略的原因被销毁啦。此时再由ActivityB回到ActivityA时,ActivityA已被销毁,而系统会再重新创建一个ActivityA' ,而ActivityA和ActivityA' 已经是完全不同的实例啦。 而回调中匿名内部类会引用外部实例的引用,所以若在onResultCallBack中更新或者处理逻辑,实际上此时已经不是ActivityA' 中的操作啦,而是被销毁的ActivityA中的操作,因此无法直接使用回调的方式。 不使用回调的原因: 1. onActivityResult确实麻烦 2. onResultCallBack确实可以简化代码的编写 3. 但Activity的销毁和恢复机制"不允许匿名内部类"出现 高级:基于注解处理器和Fragment回调实现--ActivityStarter Fragment中也有onActivityResult,并且Activity被回调回来时,Fragment中的onActivityResult也会被调用。因此现在也有些人在尝试使用Fragment中的onActivityResult替代Activity中的onActivityResult。 实现原理:添加空Fragment只ActivityA中,然后在ActivityB finish返回后使用Fragment的onActivityResult替代ActivityA中的onActivityResult。RxPermissions就是如此,而onActivityResult替代框架则为ActivityStarter或者RxActivityResult。 外部引用变换解决方案原理:在外部类引用变换时,通过反射替换匿名内部类以及回调逻辑中的的外部引用。 匿名内部类也是一个类,也可以使用反射去访问,只要通过反射拿到这个类型对应的引用,然后替换成新的引用。 如何拿到新的引用?Fragment的onActivityResult会被调用,而被调用时此Fragment已经是新的Fragment,因为Activity已经是新的啦,Fragment必然是新恢复创建的。因此可以通过新的Fragment拿到新的ActivityA,去替换新的引用。 例如:回调中处理 mTextView.setText(str); mTextView是成员变量,则可以通过上述方法完成。若改成textView.setText(str);,此时textView则是外部自由变量,无法通过外部类的引用就可以引用到的,而这种变量是直接通过构造方法直接捕获了外部的局部变量,直接拿过来一个副本。 因此此时需要找新的ActivityA中对应的textView,如何找?记录textView.getId(),通过id来找,当然可能会出现代码写的不是很好,而出现id重复,id重复会引发各种问题,比如在保存和恢复时以及此处查找出问题等,因此需要尽量避免。 现在假设id一定不会重复,我们需要在ActivityA销毁前保存textView的id,然后在新的ActivityA回调时,恢复成新的textView。 除了View之外,还有Fragment,Activity被销毁时,Fragment也会被销毁,恢复时随之恢复,所以类似的这些都需要被更新。Fragment也有类似的id,但是都不太可靠,比如contentid,但是若Fragment添加过多,还是无法确认是哪个Fragment,而tag也是可以重复的。 Fragment有一个字段叫mWho,此字段不是公开的,但却可标定Fragment的唯一身份,因此可以通过反射捕获F让个没头脑的该字段来去更新Fragment。
2. 线程安全(高并发)笔记
3. JNI 编程的细节笔记
- CPU架构适配需要注意哪些问题?
面试官视角:这道题想考察什么? 1. 是否有过Native开发经验(中级) 2. 是否关注过CPU架构适配(中级) 3. 是否有过含Native代码的SDK开发的经历(中级) 4. 是否针对CPU架构适配做过包体积优化(高级) 题目剖析: 1. Native开发才会关注CPU架构 2. 不同CPU架构之间的兼容性如何 3. so库太多如何优化Apk体积 4. SDK开发者应当提供哪些so库 题目结论: CPU架构的指令兼容性 1. mips64/mips(已废弃) 2. x86_64/x86 3. arm64-v8a/armeabi-v7a/armeabi(兼容性最好,可兼容2和3中的ALL) 兼容模式运行的一些问题: 1. 兼容模式运行的Native库无法获得最优性能 - x86_64/x86的电脑上运行arm的虚拟机会很慢 2. 兼容模式容易出现一些难以排查的内存问题 3. 系统优先加载对应架构目录下的so库(要么提供一套,要么不提供) 减小包体积优化:为App提供不同架构的Natvie库 1. 性能不敏感且无运行时异常,则可以只通过armeabi一套so库。 2. 结合目标用户群体提供合适的架构:目前现在大部分手机都是基于armeabi-v7a架构,因此可以只提供一套armeabi-v7a的so库。 3. 线上监控问题,针对性提供Native库:根据线上反馈,来决定该为哪个so库提供别的版本so库。不同版本的so库可以都放到一个目录下,然后通过判断当前设备属于哪个架构,加载不同的so库(代表作:微信)。 4. 动态加载Native库:非启动加载的库可云端下发 5. 优化so体积: 1. 默认隐藏所有符号,只公开必要的(减小符号表大小):-fvisibility=hidden 2. 禁用C++ Exception 和 RTTI:-fno-exceptions -fno-rtti 3. 不要使用iostream,应优先使用Android Log 4. 使用gc-sections去除无用代码 LOCAL_CFLAGS += -ffunction-sections -fdata-sections LOCAL_LDFLAGS += -Wl,--gc-sections 6. 构建时分包:不少应用商店已经支持按CPU架构分发安装包 splits{ abi{ enable true reset() include "armeabi-v7a","arm64-v8a","x86_64","x86" universalApk true } } SDK开发者需要注意什么? 1. 尽量不在Native层开发,降低问题跟踪维护成本 2. 尽量优化Native库的体积,降低开发者的使用成本 3. 必须提供完整的CPU架构依赖
- Java Native方法与Native函数是怎么绑定的?
面试官视角:这道题想考察什么? 1. 是否有过Native开发经验(中级) 2. 是否面对知识善于完善背后的原因(高级) 题目剖析: 1. 静态绑定:通过命名规则映射 2. 动态绑定:通过JNI函数注册 代码示例: package com.jni.study.native; public class NativeStudy{ public static native void callNativeStatic(); } // javah生成native方法的.h头文件 extern "C" JNIEXPORT void JNICALL Java_com_jni_study_native_NativeStudy_callNativeStatic(JNIEnv*,jclass) 题目结论: 一星:静态绑定:包名的.变成_ + 类名_ + 方法名(JNIEnv*,jclass/jobject) 二星:.h头文件说明 extern "C":告诉编译器,编译该Native函数时一定要按照C的规则保留这个名字,不能混淆这个函数的名字(比如:C++混编) JNIEXPORT:为编译器设置可见属性,强制在符号表中显示,优化so库时可以隐藏不需要公开的符号INVISIBLE,而此处不可隐藏需要设置为DEFAULT JNICALL:部分平台上需要(比如:mips、windows),告诉编译器函数调用的惯例是什么,比如:参数入栈以及返回清理等等 三星:动态绑定 1. 获取Class FindClass(className) 2. 注册方法 jboolean isJNIErr= env->RegisterNatives(class, methods, methodsLength) < 0; 3. 动态绑定可以在任何时刻触发 4. 动态绑定之前根据静态规则查找Native函数 5. 动态绑定可以在绑定后的任意时刻取消 6. 动态绑定和静态绑定对比: 动态绑定 静态绑定 Native函数名 无要求 按照固定规则编写且采用C的名称修饰规则 Native函数可见性 无要求 可见 动态更换 可以 不可以 调用性能 无需查找 有额外查找开销 开发体验 几乎无副作用 重构代码时较为繁琐 Android Studio支持 不能自动关联跳转 自动关联 JNI函数可跳转
- JNI如何实现数据传递?
面试官视角:这道题想考察什么? 1. 是否有过Native开发经验(中级) 2. 是否对JNI数据传递中的细节有认识(高级) 3. 是否能够合理的设计JNI的界限(高级) 题目剖析: 1. 传递什么数据? 2. 如何实现内存回收? 3. 性能如何? 4. 结合实例来分析更有效 题目结论: 一星:Bitmap Native层也有一个类对应 // 示例:Bitmap的compress方法,将Bitmap压缩到一个流中。 private final long mNativePtr; // 成员变量mNativePtr指针,对应Native层对应的Bitmap.h/cpp类 public boolean compress(CompressFormat format, int quality, OutputStream stream) { ... boolean result = nativeCompress(mNativePtr, format.nativeInt, quality, stream, new byte[WORKING_COMPRESS_STORAGE]); ... } - Native层nativeCompress方法通过传入的mNativePtr指针找到Native层对应的Bitmap.h/cpp类,然后进行压缩。 二星:字符串操作 0. 字符串操作都有一个参数 jboolean* isCopy,若返回为true则表示是从Java虚拟机内存中复制到Native内存中的。 1. GetStringUTFChars/ReleaseStringUTFChars * get返回值:const char* * 拷贝出Modified-UTF-8的字节流(字节码的格式也是Modified-UTF-8) * 若字符串里有\0字符,则\0编码成0xC080,不会影响C字符串结尾(C字符串结尾需要添加\0结束) 2. GetStringChars/ReleaseStringChars * get返回值:const jchar* * JNI函数自动处理字节序转换(Java字节序是大端,C的字节序是小端) 3. GetStringUTFRegion/GetStringRegion * 先在C层创建足够容量的空间 * 将字符串的某一部分复制到开辟好的空间 * 针对性复制,减少读取时效率更优 4. GetStringCritical/ReleaseStringCritical * 调用对中间会停止调用JVM GC * 调用对之间不可有其它JNI操作 * 调用对可嵌套 * 为何停止调用gc?防止该操作isCopy返回false,表示没有复制,而是该字符串指针指向Java虚拟机内存中, 若此时发生gc,则Java虚拟机中内存会进行整理,则该字符串指针可能变成野指针,很危险。 取决于虚拟机实现,多数总是倾向于拷贝即isCopy为true。而GetStringCritical得到原地址的可能性更高。 三星:对象数组传递 1. 访问Java对象,使用Java反射 2. 对象数组较大时,LocalRef使用有数量限制,比如:比较大的512个。使用完尽量释放env->DeleteLocalRef(obj);,若函数中使用的数量较少,也可以不释放,当方法调用完成会自动释放。 3. DirectBuffer:物理内存,Java代码写完,Native可直接使用,无需复制 Java示例: ByteBuffer buffer = ByteBuffer.allocateDifect(100); buffer.putInt(...); buffer.flip(); NativeCInf.useDifectBuffer(buffer, buffer.limit()); C示例: int * buffPtr = (int*)env->GetDirectBufferAddress(buffer); for(int i = 0; i < length / sizeof(int); i++) { LOGI("useArray:%d", buffPtr[i]); // 注意字节序 }
- 如何全局捕获Native异常?
面试官视角:这道题想考察什么? 1. 是否熟悉Linux的信号(中级) 2. 是否熟悉Native层任意位置获取jclass的方法(高级) 3. 是否熟悉底层线程与Java虚拟机的关系(高级) 4. 通过实现细节的考察,确认候选人的项目经验(高级) 题目剖析: 1. 如何捕获Native异常 2. 如何清理Navtive层和Java层的资源 3. 如何为问题的排查提供支持 题目结论:代码示例如下 JavaVM* javaVM; extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) { // Java虚拟机指针 javaVM = vm; } class JNIEnvHelper { public: JNIEnv *env; JNIEnvHelper() { needDetach = false; // 通过javaVM获取JNIEnv if(javaVM->GetEnv((void**)&env, JNI_VERSION) != JNI_OK) { // 若获取不到,则将Java虚拟机绑定到当前线程,重新获取 // 如果是Native线程,需要绑定到JVM才可以获取到JNIEnv if(javaVM->AttachCurrentThread(&env, NULL) == JNI_OK) { needDetach = true; } } } // 析构函数 ~JNIEnvHelper() { // 如果是Native线程,只有解绑时才会清理期间创建的JVM对象 if(needDetach) javaVM->DetachCurrentThread(); } private: bool needDetach; } // 程序开始时设置类加载器 static jobject classLoader; jint setUpClassLoader(JNIEnv *env) { jclass applicationClass = env->FindClass("com/jni/study/android/App"); jclass classClass = env->GetObjectClass(applicationClass); jmethodID getClassLoaderMethod = env->GetMethodID(classClass, "getClassLoader", "()Ljava/lang/ClassLoader"); // Native函数对Java虚拟机上对象的引用有什么注意事项? // jni函数获取到的引用都是本地引用(即出了引用的这个函数作用域就会被释放,若只是保存返回值则是无效的),因此需要保存则需要NewGlobalRef。 classLoader = env->NewGlobalRef(env->CallObjectMethod(applicationClass, getClassLoaderMethod)); return classLoader == NULL ? JNI_ERR : JNI_OK; } // 捕获Native异常 static struct sigaction old_signalhandlers[NSIG]; void setUpGlobalSignalHandler() { // "ThrowJNI----异常捕获"; struct sigaction handler; memset(&handler, 0, sizeof(struct sigaction)); handler.sa_sigaction = android_signal_handler; handler.sa_flags = SA_RESETHAND; #define CATCHSIG(X) sigaction(X, &handler, &old_signalhandlers[X]) CATCHSIG(SIGTRAP); CATCHSIG(SIGKILL); CATCHSIG(SIGKILL); CATCHSIG(SIGILL); CATCHSIG(SIGABRT); CATCHSIG(SIGBUS); CATCHSIG(SIGFPE); CATCHSIG(SIGSEGV); CATCHSIG(SIGSTKFLT); CATCHSIG(SIGPIPE); #undef CATCHSIG } // 传递异常到Java层 static void android_signal_handler(int signum, siginfo_t *info, void *reserved) { if(javaVM) { JNIEnvHelper jniEnvHelper; // package com.jni.study.native; jclass errorHandlerClass = findClass(jniEnvHelper.env, "com/jni/study/native/HandlerNativeError"); if(errorHandlerClass == NULL) { LOGE("Cannot get error handler class"); } else { jmethodID errorHandlerMethod = jniEnvHelper.env->GetStaticMethodID(errorHandlerClass, "nativeErrorCallback", "(I)V"); if(errorHandlerMethod == NULL) { LOGE("Cannot get error handler method"); } else { LOGE("Call java back to notify a native crash"); jniEnvHelper.env->CallStaticVoidMethod(errorHandlerClass, errorHandlerMethod, signum); } } } else { LOGE("Jni unloaded."); } old_signalhandlers[signum].sa_handler(signum); } jclass findClass(JNIEnv *env, const char* name) { if(env == NULL) return NULL; jclass classLoaderClass = env->GetObjectClass(classLoader); jmethodID loadClassMethod = env->GetMethodID(classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;"); jclass cls = static_cast<jclass>(env->CallObjectMethod(classLoader, classLoaderClass, env->NewStringUTF(name))); return cls; } // Java代码:package com.jni.study.native; public class HandlerNativeError { public static void nativeErrorCallback(int signal) { Log.e("NativeError", "[" + Thread.currentThread.getName + "] Signal:" + signal); } } // 捕获Native异常堆栈 /** * 1. 设置备用栈,防止SIGSEGV因栈溢出而出现堆栈被破坏 * 2. 创建独立线程专门用于堆栈收集并回调至Java层 * 3. 收集堆栈信息: * 1. [4.1.1——5.0) 使用内置libcorkscrew.so * 2. [5.0——至今) 使用自己编译的libunwind * 4. 通过线程关联Native异常对应的Java堆栈 /
- 只有C、C++可以编写JNI的Native库吗?
面试官视角:这道题想考察什么? 1. 是否对JNI函数绑定的原理有深入认识(高级) 2. 是否对底层开发有丰富的经验(高级) 题目剖析: 1. Native程序与Java关联的本质是什么? 2. 举例如何用其它语言编写符合JNI命名规则的符号 题目结论: 一星(按照14题):JNI对Native函数的要求 1. 静态绑定: 1. 符号表可见 2. 命名符合Java Native方法的 包名_类名_方法名 3. 符合名按照C语言的规则修饰 2. 动态绑定: 1. 函数本身无要求 2. JNI可识别入口函数如 JNI_OnLoad 进行注册即可 二星:可选的Native语言(理论上) * Golang * Rust * Kotlin Native * Scala Native * ... 三星:以Kotlin Native为例讲述如何开发