https://github.com/whatshappen/Android_Question
面试题1.
Android
开发过程中的版本适配问题?
Android4.4
适配:
uri
转path
需要适配Android5.0
适配:
分包适配 -〉在5.0
及以上在app
的gradle
文件中配置multiDexEnabled true
即可,但是5.0
以下需要倒入jar
,然后在Application
的attch
方法中进行初始化Android6.0:
权限适配 -〉敏感权限动态申请;Android7.0:
Uri.fromFile()
适配 -〉使用FileProvider
进行适配;
Android
出于安全考虑关闭了网络/拍照/录像系统广播;Android8.0:
Service
启动方式适配 -〉需要使用startForegroundService()
启动服务;
Notification
适配 -〉添加了渠道和组的概念;
软件安装适配 -〉Android8.0
去掉了“允许未知来源”选项,需要用户手动确定,所以安装程序需要在AndroidManifest.xml
文件中添加REQUEST_INSTALL_PACKAGES
权限;
广播适配 -〉AndroidManifest.xml
中注册的广播不能使用隐式,需要明确指定。
权限适配-〉读写权限分离
面试题2.
关于协程的概念
简单的介绍:协程又称微线程,是一个线程执行。协程看上去也是子程序,但是不同的是可以在子程序内部中断转而去执行其他子程序,然后在合适的时候再返回中断位置继续执行。
协程特点:
执行效率高:没有多线程的线程间切换的开销;
不需要多线程的锁机制:因为只有一个线程,所以不需要
面试题3.
synchronized
和lock
的区别?
synchronized
会主动释放锁,而lock
需要手动调用unlock
释放锁;
synchronized
是java
内置的关键字,而lock
是个java
类;
面试题4.
Handler
机制如何保证消息不错乱?消息延迟是如何实现的?Handler、Looper、MessageQueue
三者对应关系?内存泄漏如何避免?Looper
中的死循环为什么不会引器主线程ANR
?
1.handler
机制中多个handler
共有一个looper
不会错乱是因为在handler
发送消息的时候,会将当前的handler
对象绑定到message
的target
属性上,然后在Looper
取到消息后通过msg.target
拿到之前的handler
对象,然后调用handler
的handleMessage
方法。
2.消息延迟的原理:handler
发送延迟消息,会将当前的延迟时间绑定到msg
的when
属性上,然后在循环MessageQUeue
获取msg
时判断如果当前有延迟就进行阻塞,通过计时器计算时间,时间通过系统启动计算时间,然后等待阻塞时间结束之后将其唤醒,在阻塞过程中会将之后的消息放在消息队列的头部去处理。
3.同一个线程中可以有多个Handler,只有一个Looper
,而MessageQueue
在looper
中初始化的,所以也只有一个MessageQueue
。因此对应关系是:Handler:Looper = 多对一,Looper:MeesageQueue = 一对一,Handler:MessageQueue = 多对一
。
4.Handler
的内存泄漏是由于Handler
持有外部类的引用,使其无法释放。
解决办法:(1)定义成静态内部类,使其不持有外部类的引用;(2)可以使用弱引用;
还需要在外部类销毁的时候,移除所有的消息。
5.可以说整个应用的生命周期都是在looper.loop()
控制之下的(在应用启动的入口main函数中初始化ActivityThread,Handler,Looper
,然后通过handler
和looper
去控制初始化应用)。而looper.loop
采用的是Linux
的管道机制,在没有消息的时候会进入阻塞状态,释放CPU
执行权,等待被唤醒。真正会卡死主线程的操作是在回调方法onCreate/onStart/onResume
等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop
本身不会导致应用卡死。
面试题5.
开发过程中如果想替换第三方jar
中的某个class
文件,或者在开发时你的class
文件与jar
中的重名,但是你想使用自己的应该如何解决?如果你替换掉某个方法又该怎么解决?
可以获取到jar的源码或者将jar
反编译获取到java
项目,然后替换掉自己想要的.java
文件或者方法;
方式二:可以通过类加载器将目标class
替换成自己的class
;
面试题6.
IO
与NIO
的区别?
第一点:IO
是面向流的,NIO
是面向缓冲区的。
IO
面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。
NIO
是面向缓存的。数据读取到一个缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且要确保当更多的数据读入缓冲区时,不要覆盖缓冲区中未处理的数据。
第二点:IO
的各种流是阻塞的。这意味着,当一个线程调用read()
或 write()
时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情。NIO
的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,在数据可读之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO
操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)
。
面试题7.
单例模式有几种写法以及各自的优劣?
1.饿汉式:
public class SingleInstance {
private static SingleInstance mInstance = new SingleInstance();
private SingleInstance(){}
public static SingleInstance getInstance(){
return mInstance;
}
}
缺点:存在内存损耗问题,如果当前类没有用到也会被实例化
2.懒汉式:
public class SingleInstance {
private static SingleInstance mInstance = null;
private SingleInstance(){}
public static SingleInstance getInstance(){
if(mInstance==null){
synchronized (SingleInstance.class){
if(mInstance==null){
mInstance = new SingleInstance();
}
}
}
return mInstance;
}
}
缺点:加了synchronized
锁会影响性能
有次被问到为什么要有两次空判断?
第一次空判断和好理解,可以很大程度上减少锁机制的次数;
第二次判空是因为,如果a,b两个线程都到了synchronized处,而假设a拿到了锁,进入到代码块中创建了对象,然后释放了锁,由于b线程在等待锁,所以a释放后,会被b拿到,因此此时判空就保证了实例的唯一性。
3.静态内部类:
public class SingleInstance {
private SingleInstance(){}
public static SingleInstance getInstance(){
return Builder.mInstance;
}
private static class Builder{
private static SingleInstance mInstance = new SingleInstance();
}
}
优点:解决了内存浪费问题,同时也避免了加锁性能问题
为什么这种写法是线程安全的?
因为类加载过程是安全的,而静态变量是随着类的加载进行初始化的。
4.枚举形式:
public enum SingleInstance {
INSTANCE;
}
优点:不存在反射和反序列化的问题。
缺点:通过查看枚举类生成的class文件发现,有多少变量,就会在静态代码块中创建多少对象,所以不建议使用。
定义一些有意义的常量,如果不用枚举,怎么解决?
可以使用注解的形式,例如:
@IntDef({DataType.INT, DataType.STRING, DataType.FLOAT, DataType.DOUBLE, DataType.OBJECT})
public @interface DataType {
int INT = 0;
int STRING = 1;
int FLOAT = 2;
int DOUBLE = 3;
int OBJECT = 4;
}
面试题8.
ArrayList 和LinketList区别?hashmap的实现原理?hashmap与hashtable的区别?
ArrayList与LinketList差别:
ArrayList
基于数组实现,所以get
,set
操作效率较高;
LinketList
基于链表实现(双向链表),所以add,remove
操作效率较高;
如何实现高效率的查询和插入结构?
二叉树或者散列表
HashMap实现原理:
hashmap
是由数组+链表结构现实的。获取到key
的hashcode
,然后对数组长度取余,找到对应的数组位置index
,然后在对应的链表中判断是否有当前key
,从而进行查询/添加/替换等操作。
HashMap与HashTable区别:
面试题9.
gson序列化数据时如何排除某个字段?
方式一:给字段加上 transient 修饰符
方式二:排除Modifier指定类型的字段。这个方法需要用GsonBuilder定制一个GSON实例。
方式三:使用@expose注解。没有被 @expose 标注的字段会被排除
面试题10.
ButterKnife
与Xutils
注解的区别?以及Retrofit中的注解是如何处理的?
ButterKnife
采用的是编译时注解,在编译时生成辅助类,在运行时通过辅助类完成操作。编译时注解运行效率较高,不需要反射操作。
XUtils采用的是运行时注解,在运行时通过反射进行操作。运行时注解相对效率较低。
Retrofit
与EventBus
采用的都是运行时注解,也就是通过反射技术处理的。
面试题11.
jvm
的类加载机制?
类加载分类:
BootstrapClassLoader
(负责加载java_home
中的jre/lib/rt.jar
中的class
,不是ClassLoader
的子类)
ExtensionClassLoader
(负责加载java平台中扩展的一些jar中的class)
AppClassLoader
(负责加载classpath
中指定的jar
或class
文件)
CustomClassLoader
(自定义的classloader
)
JVM的类加载机制采用的是双亲委派模型。
类加载过程:
由底层类加载器开始查找是否已经加载,如果底层已经加载,则视为已经加载,上层就无需再加载,避免重复加载。如果没有加载,则向上层类加载器查找,以此类推,直到顶层类加载器。如果最后发现顶层类加载器也没有加载,则先交由顶层类加载器尝试加载,如果无法加载,则交由下层类加器加载,直至底层类加载器,如果还是无法加载,则JVM
会抛出相应的类加载异常。
面试题12.
列举一些git
版本控制的常用操作符?
面试题13.
AsyncTask
的原理以及弊端?AsyncTask
为什么要求在主线程加载,对象为什么要在主线程创建?
面试题14.
Android
开发中的屏幕适配方案?
sw
(smallestWidth
最小宽度)适配;
通过修改系统的density
值进行适配;
面试题15.
多线程中sleep
和wait
的区别?
sleep
是Thread
的静态方法;wait
是Object
中的方法;
sleep
过程中不会释放锁,不会让出系统资源;wait
会释放锁资源,将其放入等待池中,让出系统资源,让cpu
可以执行其他线程;
sleep
之后可以主动释放锁;wait
需要手动去notify
;
面试题16.
输出字符串中的第一个不重复的字符,例如:
“hello
”输出 ‘h’
“abbac”
输出 ‘c’
“abdabe”
输出‘d’
利用LinketHashMap
数组的有序性和键的唯一性来处理:
private void printChar(String source) {
if (source == null) {
return;
}
String soureTrim = source.replaceAll(" ", "");//去掉字符串中的所有空格
char[] chars = soureTrim.toCharArray();//拿到字符串对应的char[]
int length = chars.length;
//用map键的唯一性去处理记录重复数据,而选择LinkedHashMap是为了保证有序
LinkedHashMap<Character, Integer> map = new LinkedHashMap<>();
//循环检测或插入数据,然后通过value的值记录当前字符出现次数
for (int i = 0; i < length; i++) {
char key = chars[i];
Integer value = map.get(key);
if (value == null) {
map.put(key, 1);
} else {
map.put(key, value+1);
}
}
//value=1,说明只出现一次
Set<Character> keys = map.keySet();
for (Character key : keys) {
Integer integer = map.get(key);
if (integer == 1) {
System.out.println("current frist only char is = " + key);
break;
}
}
}
面试题17.
对有序int数组去重,并输出去重后的长度,并打印出来,要求时间复杂度为O(n)
,空间复杂度为O(1)。
例如:int[] array = {-1,0,0,2,4,4,4,6};
长度为:5,打印结果为:-1,0,2,4,6
面试题18.
假设有A,B,C三个线程,在A线程的执行过程中去执行B线程,并且等待B线程的执行结果,然后去执行C线程,然后当C线程执行完成后,返回结果给A线程。不阻塞线程,如何实现?(相关描述我也记不太清了,可能有些不准确,考点就是Future)
面试题19.
ThreadLocal
作用?
ThreadLocal
是一个线程内的数据存储类,可以通过它在指定线程中存储数据,并且只有在当前线程可以获取到存储的数据。通常当某些数据以线程为作用域并且不同线程具有不同的数据副本时使用。
通过查看源码可以知道,se
t方法会通过values()
方法拿到当前线程的ThreadLocal
数据(Thread
类中有个成员变量专门存储ThreadLocal
数据:ThreadLocal.Values localValues)
,在localValues
内部有个数组Object[] table
,用于存储ThreadLocal
的值,而位置存储在ThreadLocal
的reference
的下一个位置。
而get
方法就是通过当前线程的reference
拿到localValues
中table
的位置,然后index+1
获取数据。