Java集合
JAVA提高九:集合体系
JAVA提高十:ArrayList 深入分析
JAVA提高十一:LinkedList深入分析
JAVA提高十二:HashMap深入分析
JAVA提高十四:HashSet深入分析
Java提高十五:容器元素比较Comparable&Comparator深入分析
Java提高十六:TreeMap深入分析
Java提高十七:TreeSet 深入分析
使用TreeSet实现对一串字符串去重排序
TreeSet排序原理
TreeSet是实现Set接口的实现类。所以它存储的值是唯一的,同时也可以对存储的值进行排序,排序用的是二叉树原理。所以要理解这个类,必须先简单理解一下什么是二叉树。

TreeSet去重原理
如果compareTo返回0,说明是重复的,返回的是自己的某个属性和另一个对象的某个属性的差值,如果是负数,则往前面排,如果是正数,往后面排;
代码实现
public class BigIntegerUtils {
public static void main(String[] args) {
String aaa = "8367109372";
TreeSet<String> treeSet = new TreeSet<>();
for (String index: aaa.split("")) {
treeSet.add(index);
}
String st ="";
for (String resString: treeSet) {
System.out.println("print " + resString);
st = st + resString;
}
System.out.println("print result " + st);
}
结果输出
print 0
print 1
print 2
print 3
print 6
print 7
print 8
print 9
print result 01236789
Class transformation time: 0.006123735s for 209 classes or 2.930016746411483E-5s per class
Process finished with exit code 0
两个超级大的字符串数字,相加,不能使用BigInteger,Bigdecimal
算法分析
直接计算是不行的,会造成溢出。 需要每一个字符一个一个相加,相加之后再一位一位的添加到新的字符串中,但是如果两个大于五的数值相加,就会溢出一位, 相应的上一位就要+1,所以我们要将两个字符串进行反转,从小位到大位一个一个加,溢出之后,再进位+1
**实现原理**
假如两个字符串:`975626`, `1278`, 倒叙反转补全之后:`656579`, `872100`
第一次遍历:6+8=14,个位数为4,十位数进一位,我们就需要将1提出来,在下一次遍历时加上,即新的字符串为4,进位有一个1,
第二次遍历:5+7=12,个位数为2,十位数进一位,此时新的字符串为:2+(1+1), 解释一下:2[个位数2] + (1+1)[上一次进位1+本次进位1]
以此类推
代码实现
public class BigIntegerUtils {
public static void main(String[] args) {
// String str1 = "98765432117485448896565789655644";
// String str2 = "486565456165465465165156";
String str1 = "975626";
String str2 = "1278";
//第一步:反转字符串
StringBuffer strRe1 = new StringBuffer(str1).reverse();
StringBuffer strRe2 = new StringBuffer(str2).reverse();
StringBuffer resultBuffer = new StringBuffer();
int length1 = strRe1.length();
int length2 = strRe2.length();
int resultLength;
if (length1 > length2) { //证明str2字符串短
resultLength = length1; //以length1的长度为基准
int count = length1 - length2;
while (count-- > 0) {
strRe2.append("0");
}
} else {
resultLength = length2;
int count = length2 - length1;
while (count -- > 0) {
strRe1.append("0");
}
}
System.out.println("reverse str1: " + strRe1.toString() + ", str2: " + strRe2.toString() + ", resultLength: " + resultLength);
int num;
int overflow = 0;
for (int i = 0; i < resultLength; i++) {
int str1Value = Character.getNumericValue(strRe1.charAt(i)); //通过Character.getNumericValue获取`char`对应的int值
int str2Value = Character.getNumericValue(strRe2.charAt(i));
num = str1Value + str2Value + overflow;
if (num >= 10) {
overflow = 1;
num = num - 10;
}else {
overflow = 0;
}
resultBuffer.append(num);
System.out.println("num: " + num + ", strRe1Item:" + str1Value + ", strRe2Item: " + str2Value + ", overflow: " + overflow);
}
System.out.println("result "+ resultBuffer.reverse().toString());
}
}
结果输出
reverse str1: 626579, str2: 872100, resultLength: 6
num: 4, strRe1Item:6, strRe2Item: 8, overflow: 1
num: 0, strRe1Item:2, strRe2Item: 7, overflow: 1
num: 9, strRe1Item:6, strRe2Item: 2, overflow: 0
num: 6, strRe1Item:5, strRe2Item: 1, overflow: 0
num: 7, strRe1Item:7, strRe2Item: 0, overflow: 0
num: 9, strRe1Item:9, strRe2Item: 0, overflow: 0
result 976904
Class transformation time: 0.006606439s for 200 classes or 3.3032195000000005E-5s per class
Collection和HashMap,Map区别
解释定义:
Map集合:是一种键和值的映射关系(双列集合)
Collection集合:是最基本的集合接口,单列集合,只能存储一种类型的元素
https://www.cnblogs.com/EasonJim/p/7967138.html
sleep和wait的区别
- sleep是Thread类的方法,wait是Object类中定义的方法
- sleep()方法可以在任何地方使用
- wait()方法只能在sychronized方法或者synchronized块中使用
本质区别
- Thread.sleep只会让出CPU,不会导致锁行为的改变
- Object.wait不仅让出CPU,还会释放已经占有的同步资源锁。
ArrayList和LinkList的区别
ArrayList:
基于数组实现,查找快:o(1),增删慢:o(n),初始容量为10,扩容通过 System.arrayCopy 方法
- 查找快:直接通过getIndex()获取位置上的元素,增删慢:ArrrayList想要在指定位置插入或删除元素时,主要耗时的是System.arraycopy动作,会移动index后面所有的元素*
LinkedList:
基于双向链表实现,查找慢:o(n),增删快:o(1),封装了队列和栈的调用
- 查找慢:从左右元素的每个位置查找。增删快:通过循环找到响应的index下标,直接执行remove(index)或add(index)操作*
Android启动流程分析
https://cloud.tencent.com/developer/article/1356506

解释说明:
①点击桌面App图标,Launcher进程采用Binder IPC向system_server进程发起startActivity请求;
②system_server进程接收到请求后,向zygote进程发送创建进程的请求;
③Zygote进程fork出新的子进程,即App进程;
④App进程,通过Binder IPC向sytem_server进程发起attachApplication请求;
⑤system_server进程在收到请求后,进行一系列准备工作后,再通过binder IPC向App进程发送scheduleLaunchActivity请求;
⑥App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送LAUNCH_ACTIVITY消息;
⑦主线程在收到Message后,通过发射机制创建目标Activity,并回调Activity.onCreate()等方法。
⑧到此,App便正式启动,开始进入Activity生命周期,执行完onCreate/onStart/onResume方法,UI渲染结束后便可以看到App的主界面。
上面的一些列步骤简单介绍了一个APP启动到主页面显示的过程,可能这些流程中的一些术语看的有些懵,什么是Launcher,什么是zygote,什么是applicationThread…..
zygote意为“受精卵“。Android是基于Linux系统的,而在Linux中,所有的进程都是由init进程直接或者是间接fork出来的,zygote进程也不例外。
Android中Context的理解以及一个应用程序中有多少个context实例?
https://www.jianshu.com/p/9bc0c8ca6fb4
Android更新UI的方式有几种?
- Activity.runOnUiThread(Runnable)
- View.post(Runnable),View.postDelay(Runnable, long)(可以理解为在当前操作视图UI线程添加队列)
- Handler
- AsyncTask
- Rxjava
- LiveData
Glide设计精妙之处
https://juejin.im/post/5eb7af05f265da7bfc40323d
Glide的生命周期绑定:
可以控制图片的加载状态与当前页面的生命周期同步,使整个加载过程随着页面的状态而启动/恢复,停止,销毁
Glide的缓存设计:
通过(三级缓存,Lru算法,Bitmap复用)对Resource进行缓存设计
Glide的完整加载过程:
采用Engine引擎类暴露了一系列方法供Request操作
Glide三个常用操作方法:
Glide.with(this).load(url).into(imageView);
1: with() 方法主要做了如下事情:
- 获取AndroidManifest.xml文件中配置的自定义模块与@GlideModule注解标识的自定义模块,然后进行Glide配置和组件替换。
- 初始化构建Glide需要的各种配置信息,例如线程池,Bitmap池,缓存策略,Engine引擎等。
- 将Glide请求与Application或者Fragment生命周期进行绑定。
2:load()防范主要做了如下事情:
- 主要通过前面实例化的Glide与RequestManager来创建RequestBuilder,然后将传进来的参数赋值给model。
3:into()方法主要做了如下事情:
- 主要进行发起网络请求,缓存数据,解码并显示图片。 大概流程为:发起网络请求前先判断是否有内存缓存,有则直接从内存缓存中获取数据并显示,没有则判断是否有磁盘缓存,有磁盘缓存则直接从磁盘缓存哪里获取数据并显示,没有才发起网络请求,网络请求成功后将返回的数据存储到内存和磁盘缓存(如果配置了),最后将返回的输入流解码成Drawable显示在ImageView上。
Arouter实现原理
解释定义:
ARouter维护了一个路由表Warehouse,其中保存着全部的模块跳转关系,ARouter路由跳转实际上还是调用了startActivity的跳转,使用了原生的Framework机制,只是通过apt注解的形式制造出跳转规则,并人为地拦截跳转和设置跳转条件。
详细流程:
我们在代码中加入@Route注解,会在编译时通过apt生成一些存储路径以及XXXXActivity.class映射文件,然后App启动时会加载这些类文件,把这些数据读取到内存保存到map里,然后进行路由跳转时,通过build方法传入要到达的页面的路由地址,ARouter会通过存储的路由表找到对应的Activity.class(activity.class = map.get(path)),然后new Intent(context, activitity.class),当调用ARouter的withString()方法它的内部会调用intent.putExtra(String name, String value),调用navigation()方法时,内部会调用startActivity(intent)进行跳转,这样便实现了两个没有依赖的module顺利的启动对方的Activity了
什么是观察者模式
https://www.jianshu.com/p/111e0a4b9b17
解释定义:
观察者模式是一种基于事件和响应的设计模式
用Android的OnclickListener举例,基于事件就是OnclickListener监听事件,响应就是onclick()接口回调
在面向对象编程中,要尽量面向对象,而不是面向具体,这样才能减少耦合,观察者模式很好的解决了这一点,Android源码中有很多观察者模式的代码,比如最简单的OnClickListener,RecycleView.ViewHolder等等都是通过观察者模式定义的。在观察者模式中,会抽离出一个观察者(下游)对象,所有需要接收并做出响应的事件都必须实现这个接口。而事件的发起者,也就是被观察者(上游),上游存储了多个观察者(下游)集合,当事件发生时,会通知所有集合中的观察者(下游)
class Monster :Observer {
override fun update() {
if (isRange()) {
println("主角遭遇怪物,怪物来袭")
}
}
override fun isRange(): Boolean {
return true
}
}
class Trap :Observer {
override fun update() {
if (isRange()) {
println("主角遭遇陷阱,完蛋了")
}
}
override fun isRange(): Boolean {
return true
}
}
class Blood :Observer {
override fun update() {
if (isRange()) {
println("主角遇到血包,给主角加血")
}
}
override fun isRange(): Boolean {
return true
}
}
open class Subject {
private val subjects = mutableListOf<Observer>()
fun attachObserver(observer: Observer) {
subjects.add(observer)
}
fun detachObserver(observer: Observer) {
subjects.remove(observer)
}
fun notifyObserver() {
for (observer in subjects) {
observer.update()
}
}
}
class Hero :Subject(){
fun moveing() {
println("主角向前移动,,,")
notifyObserver()
}
}
fun main() {
val hero = Hero()
hero.attachObserver(Monster())
hero.attachObserver(Trap())
hero.attachObserver(Blood())
hero.moveing()
}
主角向前移动,,,
主角遭遇怪物,怪物来袭
主角遭遇陷阱,完蛋了
主角遇到血包,给主角加血
可以看到,当主角(上游)向前移动(执行事件时),会同时响应怪物,陷阱,血包(下游),如果再来很多下游也是可以同时响应的,这就是所谓的观察者
Rxjava 操作符
Android RxJava: 这是一份全面的 操作符 使用汇总 (含详细实例讲解)
Rxjava map和flatMap区别
- map 把发射对象转换成另外一个对象发射出去
val listTest = arrayListOf<String>("1111", "2222", "3333")
Observable.just(listTest)
.map {
val list2 = mutableListOf<Int>()
it.forEach {
list2.add(it.toInt())
}
list2
}.subscribe {
it.forEach {
println("打印结果 it=$it")
}
}
- flatMap
把发射对象转换成另外一个Observable,从而把这个Observable的对象发射出去
val listTest = arrayListOf<String>("1111", "2222", "3333")
Observable.just(listTest)
.flatMap {
val list2 = mutableListOf<Int>()
it.forEach {
list2.add(it.toInt())
}
Observable.just(list2)
}.subscribe {
it.forEach {
println("打印结果 it=$it")
}
}
Rxjava是如何实现线程切换的?
Scheduler是所有线程调度的抽象父类,在Scheduler中的scheduleDirect方法中有一个Worker,我们就拿newThread()来说,newThread创建了一个NewThreadScheduler,继承自Scheduler。复写了createWorker方法,
createWorker方法中创建了一个NewThreadWorker类。NewThreadWorker中创建了一个ScheduledExecutorService线程池,该executor最多只有1个线程执行,在这个线程池中执行新任务,也就是完成了线程切换。
源代码理解:
public final class NewThreadScheduler extends Scheduler {
final ThreadFactory threadFactory;
...
public NewThreadScheduler(ThreadFactory threadFactory) {
this.threadFactory = threadFactory;
}
@NonNull
@Override
public Worker createWorker() {
return new NewThreadWorker(threadFactory);
}
}
public class NewThreadWorker extends Scheduler.Worker implements Disposable {
private final ScheduledExecutorService executor;
...
public NewThreadWorker(ThreadFactory threadFactory) {
// 每次都新建一个线程池,该线程池只有一个线程运行
executor = SchedulerPoolFactory.create(threadFactory);
}
@NonNull
@Override
public Disposable schedule(
@NonNull final Runnable action, long delayTime, @NonNull TimeUnit unit) {
...
return scheduleActual(action, delayTime, unit, null);
}
@NonNull
public ScheduledRunnable scheduleActual(
final Runnable run, long delayTime,
@NonNull TimeUnit unit, @Nullable DisposableContainer parent) {
Runnable decoratedRun = RxJavaPlugins.onSchedule(run);
ScheduledRunnable sr = new ScheduledRunnable(decoratedRun, parent);
...
return sr;
}
}
AndroidSchedulers.mainThread()
AndroidSchedulers.mainThread() 是 RxAndroid 提供的用于切换到 Android 主线程(通常是 UI 线程) 的调度器,我们可以在后台线程中执行一些耗时操作,再切换到主线程更新 UI.
RxAndroid 的封装非常简单,就是基于 Handler 的实现:
final class HandlerScheduler extends Scheduler {
// handler 是与 Looper.getMainLooper() 绑定的 Handler 实例
private final Handler handler;
private final boolean async;
...
@Override
public Disposable scheduleDirect(Runnable run, long delay, TimeUnit unit) {
...
run = RxJavaPlugins.onSchedule(run);
ScheduledRunnable scheduled = new ScheduledRunnable(handler, run);
// 延时调度
handler.postDelayed(scheduled, unit.toMillis(delay));
return scheduled;
}
@Override
public Worker createWorker() {
return new HandlerWorker(handler, async);
}
}
同样地,我们再来看看 HandlerWorker, 也是基于 Handler 实现的:
private static final class HandlerWorker extends Worker {
private final Handler handler;
private final boolean async;
...
// Async will only be true when the API is available to call.
@Override
@SuppressLint("NewApi")
public Disposable schedule(Runnable run, long delay, TimeUnit unit) {
...
ScheduledRunnable scheduled = new ScheduledRunnable(handler, run);
Message message = Message.obtain(handler, scheduled);
// Used as token for batch disposal of this worker's runnables.
message.obj = this;
if (async) {
message.setAsynchronous(true);
}
// 延迟调度
handler.sendMessageDelayed(message, unit.toMillis(delay));
...
}
}
注意,上述代码中有一个 async 变量,设置为 true 时可以提升 UI 性能(如果你需要频繁调度到 Android 主线程的话)
Java类加载过程
解释定义:
加载-验证-准备-解析-初始化
加载:是把class字节码文件从各个来源通过类加载器装载入内存中。
验证:主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
准备:主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。
解析:将常量池内的符号引用替换为直接引用的过程。
初始化:主要是对类变量初始化,是执行类构造器的过程
Java 中堆和栈有什么区别?
解释定义:
JVM 中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局部变量,而对象总是在堆上分配。栈通常都比堆小,也不会在多个线程之间共享,而堆被整个 JVM 的所有线程共享。
taskAffinity 会默认使 Activity 在新的栈中分配吗?
单纯使用 taskAffinity 不能导致 Activity 被创建在新的任务栈中,需要配合 singleTask 或者 singleInstance!
简单谈一下Java堆的垃圾回收机制?
解释定义:
当通过 Java 命令启动 Java 进程的时候,会为它分配内存。内存的一部分用于创建堆空间,当程序中创建 对象的时候,就从对空间中分配内存。GC 是 JVM 内部的一个进程,回收无效对象的内存用于将来的分配。
程序运行时,内存到底是如何进行分配的?
很多人将Java的内存分为堆内存(heap)和栈内存(Stack),这种划分其实并不完全准确。
Java的内存划分实际远比这复杂,下图描述了一个HelloWord.java文件被JVM加载到内存中的过程:

- 1 HelloWord.java文件首先需要经过编译器编译,生成HelloWord.class字节码文件。
- 2 Java程序中访问HelloWord这个类时,需要通过ClassLoader(类加载器)将HelloWord.class加载到JVM内存中
- 3 JVM中的内存可以划分为:程序计数器、虚拟机栈、本地方法栈、堆、方法区
程序计数器
程序计数器是虚拟机中一块比较小的内存,主要用于记录当前线程执行的位置。 当某一个线程被CPU挂起时,需要记录代码已经执行到的位置,在重新执行此线程时,知道从哪行指令开始执行。
虚拟机栈
本地方法栈
本地方法栈主要存储一些局部变量,程序状态等。
堆
一般存储一些实例化(也就是new)出来的对象。
方法区
主要存储一些类信息,常量,静态变量等。
GC回收机制和分代回收策略
所谓垃圾就是内存中已经没用的对象。既然是“垃圾回收”,就必须知道那些对象是垃圾。Java虚拟机中使用一种“可达性分析”的算法来决定对象是否被回收。
如何确定一个对象是否可以被回收?
1)引用计数法
解释定义 引用计数算法是早期的一种计算策略,是通过判断对象的引用数量来决定对象是否可以被回收。
在这种方法中,堆中的每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个引用变量,该对象实例的引用计数设置为 1。当任何其它变量被赋值为这个对象的引用时,对象实例的引用计数加 1(a = b,则b引用的对象实例的计数器加 1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数减 1。特别地,当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器均减1。任何引用计数为0的对象实例可以被当作垃圾收集。
2)可达性分析法
解释定义 可达性分析算法是通过判断对象的引用链是否可达来决定对象是否可以被回收。
可达性分析算法是从离散数学中引入的。JVM把内存中所有对象之间的引用关系看做一张图,通过一组名为“GC Root”的对象作为起点,从这些节点向下搜索,搜索所过的路径被称为引用链,最后通过判断对象的引用链是否可达来决定对象是否被回收。如图所示:

上图中,对象A、B、C、D、E与GC Root之间存在一条直接会间接的引用链,这也说明他们与GC Root是可达的,因此他们是不能被GC回收的。而对象M、K虽然被J引用,但是与GC Root并未通过引用链链接,所以当GC进行垃圾回收时,遍历到J、K、M对象时就会被回收
垃圾收集算法
1)标记清除法
解释定义 标记清除算法分为标记和清除两个阶段。该算法首先从根集合进行扫描,对存活的对象进行标记,再扫描整个空间中未被标记的对象进行回收
2)复制算法
解释定义 复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
3)分代回收法 解释定义 对于一个大型的系统,当创建的对象和方法变量比较多时,堆内存中的对象也会比较多,如果逐一分析对象是否该回收,那么势必造成效率低下。分代收集算法是基于这样一个事实:不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率。当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就采用复制算法;老年代存活率高,就用标记清除算法或者标记整理算法。Java堆内存一般可以分为新生代、老年代和永久代三个模块
1:新生代 新生代的目标就是尽可能快速的收集掉那些生命周期短的对象,一般情况下,所有新生成的对象首先都是放在新生代的。
2:老年代 老年代存放的都是一些生命周期较长的对象,就像上面所叙述的那样,在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到老年代中。
3:永久代 永久代主要用于存放静态文件,如Java类、方法等。
ClassLoader的加载机制
一个完整的程序是由多个.class文件组成的,在程序运行过程中,需要将这些.class文件加载到JVM中才可以使用。类加载器(ClassLoader)就负责这些.class的加载。
Java中的类何时被加载器加载
程序启动时,并不会一次性加载程序中的所有.class文件,而是在程序运行过程中,动态加载相应的类到内存中的。 通常情况下,Java程序中的.class文件会在以下2种情况下被ClassLoader加载到内存中:
1、 调用类的构造方法,也就是new对象时
2、 调用类中的静态(static)变量或静态方法时
Java中三种类加载器(ClassLoader):
1、启动类加载器 BootstrapClassLoader
2、扩展类加载器 ExtClassLoader (JDK 1.9 之后,改名为 PlatformClassLoader)
3、系统加载器 APPClassLoader
Java的volatile关键字
https://crossoverjie.top/2018/03/09/volatile/
https://juejin.im/post/6844903520760496141
为什么会出现这个关键字?
在Java中,所有的变量都是放到主内存中的,而每一个线程都有自己的工作内存(高速缓存,工作内存效率更高),线程在运行过程中,需要将主内存中的数据拷贝到工作内存,等将数据操作更新完毕之后再更新到主内存中。
主内存认为是堆内存,工作内存是栈内存
主内存中的一个值在并发运行时可能会出现线程 B 所读取到的数据是线程 A 更新之前的数据。 显然这样肯定是有问题的,这时候volatile 的作用出现了:
当一个变量被volatile修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让工作内存中的该变量的线程中的数据清空,必须从主内存重新读取最新数据。 volatile修饰的共享变量,就具有了以下两点特性:
1、 保证了不同线程对该变量操作的内存可见性;
2、 禁止指令重排序
简单谈一下类加载的双亲委托机制?
解释定义:
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的双亲委派模式.那么这种模式有什么作用呢?
好处
-
1.避免重复加载 Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
-
2.防止核心Api被随意篡改 java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改 采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象
Handler
解释定义:
Handler的引入根本上就是为了解决多线程并发问题 在程序启动时,Android会同时启动一条主线程(ActivityThread)来负责处理与UI相关的事件,也就是我们常说的UI线程 只要有异步线程与主线程通信的地方就一定会有 Handler Handler 都会跟一个线程绑定,并与该线程的 MessageQueue 创建Handler之前一定会先创建Looper,每一个线程内最多只有一个Looper, 如在创建Handler前不做好Looper.prepare(),则会抛出异常

例子:
比如:报告老师,我要上洗手间。然后老师说:允许,快去吧。
过了一会。
又给老师报告:报告老师,后面有同学踢我凳子。老师说:你先坐前面来,不要理他。
当然,这只是比喻。我们通过这个比喻,来比较生动形象的去理解handler就容易多了。
1.handler:学生。
2.message:报告老师的内容
3.Looper :老师自己
4.messagequeue: 老师的耳朵和听力记忆
解释:
学生(handler)向老师(Looper)举手
说 “我要上洗手间”这个报告(message),
然后老师的耳朵(messagequeue)听到了这个报告,
先是反馈了学生的请求(dispatchHandle),告诉这个同学,同意他的请求,
于是学生自己就上洗手间去了(handleMessage)
Handler是如何和线程关联的?
Handler实际是与Lopper关联,跟线程的关联也是通过Lopper来实现的。
在我们实例化Handler的时候会先去检查当前线程的Lopper是否存在,如果不存在则会报异常,也就是说在创建Handler之前一定会先创建Lopper.
Lopper提供了Lopper.prepare()方法来创建Lopper,并且会借助ThreadLocal来实现与当前线程的绑定,Lopper.lopp()会不断尝试从MessageQueue中获取Message,并分发给对应的Handler.
Handler的线程切换是怎么回事?
Looper 死循环为什么不会导致应用卡死,会消耗大量资源吗?
https://zhuanlan.zhihu.com/p/73690640
解释定义:
涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。 Gityuan–Handler(Native层)
handler.postDelay()的具体实现?
解释定义:
handler.postDelay()并不是先等待一定的时间再放入到MessageQueue中,而是直接进入MessageQueue,以MessageQueue的时间顺序排列和唤醒的方式结合实现的。
实现原理:
-
1.消息是通过MessageQueen中的enqueueMessage()方法加入消息队列中的,并且它在放入中就进行好排序,链表头的延迟时间小,尾部延迟时间最大
-
2.Looper.loop()通过MessageQueue中的next()去取消息
-
3.next()中如果当前链表头部消息是延迟消息,则根据延迟时间进行消息队列会阻塞,不返回给Looper message,知道时间到了,返回给message
-
4.如果在阻塞中有新的消息插入到链表头部则唤醒线程
-
5.Looper将新消息交给回调给handler中的handleMessage后,继续调用MessageQueen的next()方法,如果刚刚的延迟消息还是时间未到,则计算时间继续阻塞
post()与sendMessage()区别?
- 1.post和sendMessage功能其实差不多,post其实也是通过sendMessage来实现的,都是发送消息到Handler所在的线程的消息队列中
- 2.post的用法更方便,经常会post一个Runnable,处理的代码直接写在Runnable的run方法中,其实就是将这个Runnable发送到Handler所在线程(一般是主线程)的消息队列中。sendMessage方法主线程处理方法一般则是写在handleMessage中
view.post()和handler.post()区别
- view.post其实内部是获取到了view所在线程(即ui线程)的handler,并且调用了handler的post方法
Handler时怎么解决内存泄漏的问题的?
Handler 允许我们发送延时消息,如果在延时期间用户关闭了 Activity,那么该 Activity 会泄露。 这个泄露是因为 Message 会持有 Handler,而又因为 Java 的特性,内部类会持有外部类,使得 Activity 会被 Handler 持有,这样最终就导致 Activity 泄露。
解决:将 Handler 定义成静态的内部类,在内部持有 Activity 的弱引用,并在Acitivity的onDestroy()中调用handler.removeCallbacksAndMessages(null)及时移除所有消息。
强引用,软引用,弱引用和虚引用的区别
强引用
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如下:
Object strongReference = new Object();
软引用
如果一个对象只具有软引用,则内存控件充足时,垃圾回收期就不会回收它,如果内存不足时就会回收这些对象。只要垃圾回收期没有回收它,该对象就可以被程序使用。
// 强引用
String strongReference = new String("abc");
// 软引用
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<String>(str);
弱引用
弱引用的对象,在垃圾回收器线程扫描内存区域时,一旦发现具有弱引用的对象,不管当前内存控件足够与否,都会回收内存。
String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
str = null;
虚引用
虚引用顾名思义,就是形同虚设。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收期回收。
String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(str, queue);
HandlerThread
https://www.jianshu.com/p/5b6c71a7e8d7
解释定义:HandlerThread是Thread的一个子类,HandlerThread自带Looper使他可以通过消息队列来重复使用当前线程,节省系统资源开销。这是它的优点也是缺点,每一个任务都将以队列的方式逐个被执行到,一旦队列中有某个任务执行时间过长,那么就会导致后续的任务都会被延迟处理。
LiveData
解释定义: LiveData是一个可以被观察的数据持有者类。
特点:
1)采用观察者模式自动提示UI更新。
2)不需要手动处理生命周期,不会因为Activity的销毁重建而丢失数据。
3)不会出现内存泄漏。
4)不需要手动取消订阅,Activity在非活跃状态下(销毁、finish之后)不会收到数据更新信息。
为什么LiveData可以主动更新UI线程?
LiveData是一个泛型类,泛型参数就是存储数据的实际类型,同时MutableLiveData提供两个赋值方法:postValue和setValue
1)setValue方法
protected void setValue(T value) {
assertMainThread("setValue"); //1
mVersion++; //2
mData = value; //3
dispatchingValue(null); //4
}
private static void assertMainThread(String methodName) {
if (!ArchTaskExecutor.getInstance().isMainThread()) {
throw new IllegalStateException("Cannot invoke " + methodName + " on a background"
+ " thread");
}
}
- 首先调用 assertMainThread() 方法来判断当前线程是否为主线程(这里他通过一个ArchTaskExecutor的单例类来实现),如果不是主线程,直接抛异常。
- 如果是在主线程中调用该方法,自加加一个version,来说明值发生了变化。
- 再把新的值保存起来。
- 调用 dispatchingValue()方法并传入null,将数据分发给所有观察者。dispatchingValue如果传入null则是所有的观察者,如果是具体的ObserverWrapper对象,则通知到具体的Observer。
2)postValue方法
protected void postValue(T value) {
boolean postTask;
synchronized (mDataLock) {
postTask = mPendingData == NOT_SET;
mPendingData = value;
}
if (!postTask) {
return;
}
ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}
postValue方法可以在非主线程中更新数据。主要是通过ArchTaskExecutor和DefaultTaskExecutor切换到主线程中。然后回调到Runnable方法中,在Runnable的run方法其实也是调用了setValue方法来实现通知观察者更新数据的逻辑的。
Rxjava Map和FlatMap的区别
- map
把发射对象转换成另外一个对象发射出去
val listTest = arrayListOf<String>("1111", "2222", "3333")
Observable.just(listTest)
.map {
val list2 = mutableListOf<Int>()
it.forEach {
list2.add(it.toInt())
}
list2
}.subscribe {
it.forEach {
println("AAAAAAAAAAAAAAAAA it=$it")
}
}
- flatMap
把发射对象转换成另外一个Observable,从而把这个Observable的对象发射出去
val listTest = arrayListOf<String>("1111", "2222", "3333")
Observable.just(listTest)
.flatMap {
val list2 = mutableListOf<Int>()
it.forEach {
list2.add(it.toInt())
}
Observable.just(list2)
}.subscribe {
it.forEach {
println("AAAAAAAAAAAAAAAAA it=$it")
}
}
}
Kotlin委托
SharedPrefrence的apply()和commit()的区别
- commit方法有返回值,设置成功为ture,否则为false,applay()无返回值
- commit对一个SharedPreferences设置值最后一次的设置会直接覆盖前次值
- 如果不关心设置成功与否,并且是在主线程设置值,建议用apply方法 总结:commit相对与apply效率较低,commit直接是向物理介质写入内容,而apply是先同步将内容提交到内存,然后在异步的向物理介质写入内容。这样做显然提高了效率。
安卓面试基础知识 安卓面试基础知识