2022个人总结面试汇总

256 阅读23分钟

一、Java篇

1.JVM运行时内存分布

JVM运行时内存分布.png

Java运行时内存占用.png

2.垃圾回收

Java 虚拟机中使用一种叫作"**可达性分析”**的算法来决定对象是否可以被回收。

image.png

在 Java 中,有以下几种对象可以作为 GC Root:

(1)Java 虚拟机栈(局部变量表)中的引用的对象。
(2)方法区中静态引用指向的对象。
(3)仍处于存活状态中的线程对象。
(4)Native 方法中 JNI 引用的对象。

3.对象的初始化顺序如下:

静态变量/静态代码块 -> 普通代码块 -> 构造函数

  1. 父类静态变量和静态代码块;
  2. 子类静态变量和静态代码块;
  3. 父类普通成员变量和普通代码块;
  4. 父类的构造函数;
  5. 子类普通成员变量和普通代码块;
  6. 子类的构造函数。

1.final

(1)修饰类、变量和方法。

修饰类代表这个类不可被继承。 修饰变量代表此变量不可被改变。 修饰方法表示此方法不可被重写 (override)。

(2)为什么Java里的匿名内部类只能访问final修饰的外部变量?

因为匿名内部类最终会编译成一个单独的类,而被该类使用的变量会以构造函数参数的形式传递给该类,例如:Integer paramInteger,如果变量不定义成final的,paramInteger在匿名内部类被可以被修改,进而造成和外部的paramInteger不一致的问题,为了避免这种不一致的情况,因次Java规定匿名内部类只能访问final修饰的外部变量。

2.volatile

是Java另一种解决可见性(保证原子性)和有序性问题的方案。
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”。lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

 (1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
(2)它会强制将对缓存的修改操作立即写入主存;
(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

3.HashMap,ConcurrentHashMap原理

hash算法解析
HashMap、ConcurrentHashMap 1.7/1.8实现原理

HashMap 1.7的原理:

HashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。

负载因子:

  • 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
  • 因此通常建议能提前预估 HashMap 的大小最好,尽量的减少扩容带来的性能损耗。 其实真正存放数据的是 Entry<K,V>[] table,Entry 是 HashMap 中的一个静态内部类,它有key、value、next、hash(key的hashcode)成员变量。

put 方法:

  • 判断当前数组是否需要初始化。
  • 如果 key 为空,则 put 一个空值进去。
  • 根据 key 计算出 hashcode。
  • 根据计算出的 hashcode 定位出所在桶。
  • 如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值。
  • 如果桶是空的,说明当前位置没有数据存入,新增一个 Entry 对象写入当前位置。(当调用 addEntry 写入 Entry 时需要判断是否需要扩容。如果需要就进行两倍扩充,并将当前的 key 重新 hash 并定位。而在 createEntry 中会将当前位置的桶传入到新建的桶中,如果当前桶有值就会在位置形成链表。)

get 方法:

  • 首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。
  • 判断该位置是否为链表。
  • 不是链表就根据 key、key 的 hashcode 是否相等来返回值。
  • 为链表则需要遍历直到 key 及 hashcode 相等时候就返回值。
  • 啥都没取到就直接返回 null 。

HashMap 1.8的原理:

当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N),因此 1.8 中重点优化了这个查询效率。 TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。 HashEntry 修改为 Node。

put 方法:

  • 判断当前桶是否为空,空的就需要初始化(在resize方法中会判断是否进行初始化)。
  • 根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。
  • 如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。
  • 如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
  • 如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。
  • 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
  • 如果在遍历过程中找到 key 相同时直接退出遍历。
  • 如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。
  • 最后判断是否需要进行扩容。

get 方法:

  • 首先将 key hash 之后取得所定位的桶。
  • 如果桶为空则直接返回 null 。
  • 否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。
  • 如果第一个不匹配,则判断它的下一个是红黑树还是链表。
  • 红黑树就按照树的查找方式返回值。
  • 不然就按照链表的方式遍历匹配返回值。 修改为红黑树之后查询效率直接提高到了 O(logn)。但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现死循环:
  • 在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操作容易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环:在 1.7 中 hash 冲突采用的头插法形成的链表,在并发条件下会形成循环链表,一旦有查询落到了这个链表上,当获取不到值时就会死循环。

ConcurrentHashMap 1.7原理:

ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。

put 方法:

首先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的 put。

  • 虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。
  • 首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁: 尝试自旋获取锁。如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。
  • 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
  • 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
  • 为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
  • 最后会使用unlock()解除当前 Segment 的锁。

get 方法:

  • 只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。
  • 由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
  • ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。

ConcurrentHashMap 1.8原理:

1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题:那就是查询遍历链表效率太低。和 1.8 HashMap 结构类似:其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

CAS:

如果obj内的value和expect相等,就证明没有其他线程改变过这个变量,那么就更新它为update,如果这一步的CAS没有成功,那就采用自旋的方式继续进行CAS操作。 问题:

  • 目前在JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
  • 如果CAS不成功,则会原地自旋,如果长时间自旋会给CPU带来非常大的执行开销。

put 方法:

  • 根据 key 计算出 hashcode 。
  • 判断是否需要进行初始化。
  • 如果当前 key 定位出的 Node为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  • 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  • 如果都不满足,则利用 synchronized 锁写入数据。
  • 最后,如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。 get 方法:
  • 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
  • 如果是红黑树那就按照树的方式获取值。
  • 就不满足那就按照链表的方式遍历获取值。 1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。

HashMap何时扩容:

当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值---即大于当前数组的长度乘以加载因子的值的时候,就要自动扩容。

扩容的算法是什么:

扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组。

Hashmap如何解决散列碰撞(必问)?

Java中HashMap是利用“拉链法”处理HashCode的碰撞问题。在调用HashMap的put方法或get方法时,都会首先调用hashcode方法,去查找相关的key,当有冲突时,再调用equals方法。hashMap基于hasing原理,我们通过put和get方法存取对象。当我们将键值对传递给put方法时,他调用键对象的hashCode()方法来计算hashCode,然后找到bucket(哈希桶)位置来存储对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当碰撞发生了,对象将会存储在链表的下一个节点中。hashMap在每个链表节点存储键值对对象。当两个不同的键却有相同的hashCode时,他们会存储在同一个bucket位置的链表中。键对象的equals()来找到键值对。

Hashmap底层为什么是线程不安全的?

  • 并发场景下使用时容易出现死循环,在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操作容易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环;
  • 在 1.7 中 hash 冲突采用的头插法形成的链表,在并发条件下会形成循环链表,一旦有查询落到了这个链表上,当获取不到值时就会死循环。

4.线程池: 调度, 拒绝策略。四种线程池特点及使用场景

核心线程->阻塞队列->非核心线程->handler拒绝提交
image
有关饱和策略RejectedExecutionHandler:
(1).DiscardPolicy:不能执行任务,并将该任务删除。
(2).DiscardOldestPolicy:丢弃队列最近的任务,并执行当前任务。
(3).AbortPolicy(默认):表示无法处理新任务,并抛出RejectedExecutionException异常。
(4).CallerRunsPolicy:用调用者所在的线程来处理新任务。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
FixedThreadPool:可重用固定线程数的线程池。
CachedThreadPool:根据需要创建的线程池。
SingleThreadPool:使用单个工作线程的线程池。
ScheduledTreadPool:能实现定时和周期性任务的线程池。

5.有关泛型擦除

(1)www.cnblogs.com/wuqinglong/… (2)www.cnblogs.com/coprince/p/…

6.递归

www.cnblogs.com/renxiaoren/…

7.单例模式

juejin.im/post/5acf49…

二、Android相关

1.RecyclerView相关

(1)绘制流程:

image

(2)缓存复用原理

image

2.Handler机制

Android 的消息机制主要是指 Handler 的运行机制以及 Handler 所附带的 MessageQueue 和 Looper 的工作流程。消息的处理流程是这样的:
(1)通过 Handler 的 sendXXX 或 postXXX 系列方法发送一条消息
(2)这条消息插入到 MessageQueue 中的指定位置
(3)Looper.loop() 不断的调用 MessageQueue.next() 取出当前需要处理的消息,若当前无消息则阻塞到有新消息
(4)取出来的消息,通过 Handler.dispatchMessage 进行分发处理

3.安卓系统启动流程

Android系统启动流程是什么?(提示:init进程 -> Zygote进程 –> SystemServer进程 –> 各种系统服务 –> 应用进程)
Android系统启动的核心流程如下:
1、启动电源以及系统启动:当电源按下时引导芯片从预定义的地方(固化在ROM)开始执行,加载引导程序BootLoader到RAM,然后执行。
2、引导程序BootLoader:BootLoader是在Android系统开始运行前的一个小程序,主要用于把系统OS拉起来并运行。
3、Linux内核启动:当内核启动时,设置缓存、被保护存储器、计划列表、加载驱动。当其完成系统设置时,会先在系统文件中寻找init.rc文件,并启动init进程。
4、init进程启动:初始化和启动属性服务,并且启动Zygote进程。
5、Zygote进程启动:创建JVM并为其注册JNI方法,创建服务器端Socket,启动SystemServer进程。
6、SystemServer进程启动:启动Binder线程池和SystemServiceManager,并且启动各种系统服务。
7、Launcher启动:被SystemServer进程启动的AMS会启动Launcher,Launcher启动后会将已安装应用的快捷图标显示到系统桌面上。

4.App启动流程

1.用户点击App图标,Lanuacher进程通过Binder联系到System Server进程发起startActivity。
2.System Server通过Socket联系到Zygote,fork出一个新的App进程。
3.创建出一个新的App进程以后,Zygote启动App进程的ActivityThread#main()方法。
4.在ActivtiyThread中,调用AMS进行ApplicationThread的绑定。
5.AMS发送创建Application的消息给ApplicationThread,进而转交给ActivityThread中的H,它是一个Handler,接着进行Application的创建工作。
6.AMS以同样的方式创建Activity,接着就是大家熟悉的创建Activity的工作了。

5.apk打包流程

(1)通过AAPT工具进行资源文件(包括AndroidManifest.xml、布局文件、各种xml资源等)的打包,生成R.java文件。
(2)通过AIDL工具处理AIDL文件,生成相应的Java文件。
(3)通过Java Compiler编译R.java、Java接口文件、Java源文件,生成.class文件。
(4)通过dex命令,将.class文件和第三方库中的.class文件处理生成classes.dex,该过程主要完成Java字节码转换成Dalvik字节码,压缩常量池以及清除冗余信息等工作。
(5)通过ApkBuilder工具将资源文件、DEX文件打包生成APK文件。
(6)通过Jarsigner工具,利用KeyStore对生成的APK文件进行签名。
(7)如果是正式版的APK,还会利用ZipAlign工具进行对齐处理,对齐的过程就是将APK文件中所有的资源文件距离文件的起始距位置都偏移4字节的整数倍,这样通过内存映射访问APK文件的速度会更快,并且会减少其在设备上运行时的内存占用

6.APK的安装流程

(1)复制APK到/data/app目录下,解压并扫描安装包。
(2)资源管理器解析APK里的资源文件。
(3)解析AndroidManifest文件,并在/data/data/目录下创建对应的应用数据目录。
(4)然后对dex文件进行优化,并保存在dalvik-cache目录下。
(5)将AndroidManifest文件解析出的四大组件信息注册到PackageManagerService中。
(6)安装完成后,发送广播。
总结:记录首先Android点击APK安装,会启动PackageInstallerActivity,然后将APK信息传给PMS,PMS会做两件事,拷贝安装包和装载代码。在拷贝安装包过程中会开启Service来copyAPK,并且会检查apk安装路径,包的状态,最终以base.apk形式存在/data/app包名下。装载代码过程中,会继续解析apk,把清单文件内容存放于PMS,然后对apk进行签名校验,再执行dex2oat优化,安装成功后,更新应用设置权限,发送广播通知桌面显示APP图标,安装失败则删除安装包和各种缓存文件。

7.Android里的内存缓存和磁盘缓存是怎么实现的

内存缓存基于LruCache实现,磁盘缓存基于DiskLruCache实现。这两个类都基于Lru算法和LinkedHashMap来实现。

LruCache

LRU算法可以用一句话来描述,如下所示:

LRU是Least Recently Used的缩写,最近最少使用算法,从它的名字就可以看出,它的核心原则是如果一个数据在最近一段时间没有使用到,那么它在将来被访问到的可能性也很小,则这类数据项会被优先淘汰掉。

LruCache原理

之前,我们会使用内存缓存技术实现,也就是软引用或弱引用,在Android 2.3(APILevel 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。
其实LRU缓存的实现类似于一个特殊的栈,把访问过的元素放置到栈顶(若栈中存在,则更新至栈顶;若栈中不存在则直接入栈),然后如果栈中元素数量超过限定值,则删除栈底元素(即最近最少使用的元素)。
它的内部存在一个 LinkedHashMap 和 maxSize,把最近使用的对象用强引用存储在 LinkedHashMap 中,给出来 put 和 get 方法,每次 put 图片时计算缓存中所有图片的总大小,跟 maxSize 进行比较,大于 maxSize,就将最久添加的图片移除,反之小于 maxSize 就添加进来。 LruCache的原理就是利用LinkedHashMap持有对象的强引用,按照Lru算法进行对象淘汰。具体说来假设我们从表尾访问数据,在表头删除数据,当访问的数据项在链表中存在时,则将该数据项移动到表尾,否则在表尾新建一个数据项。当链表容量超过一定阈值,则移除表头的数据。
详细来说就是LruCache中维护了一个集合LinkedHashMap,该LinkedHashMap是以访问顺序排序的。当调用put()方法时,就会在结合中添加元素,并调用trimToSize()判断缓存是否已满,如果满了就用LinkedHashMap的迭代器删除队头元素,即近期最少访问的元素。当调用get()方法访问缓存对象时,就会调用LinkedHashMap的get()方法获得对应集合元素,同时会更新该元素到队尾。

put方法核心逻辑

在添加过缓存对象后,调用trimToSize()方法,来判断缓存是否已满,如果满了就要删除近期最少使用的对象。trimToSize()方法不断地删除LinkedHashMap中队头的元素,即近期最少访问的,直到缓存大小小于最大值(maxSize)。

get方法核心逻辑

当调用LruCache的get()方法获取集合中的缓存对象时,就代表访问了一次该元素,将会更新队列,保持整个队列是按照访问顺序排序的。 为什么会选择LinkedHashMap呢? 这跟LinkedHashMap的特性有关,LinkedHashMap的构造函数里有个布尔参数accessOrder,当它为true时,LinkedHashMap会以访问顺序为序排列元素,否则以插入顺序为序排序元素。

LinkedHashMap原理

LinkedHashMap 几乎和 HashMap 一样:从技术上来说,不同的是它定义了一个 Entry<K,V> header,这个 header 不是放在 Table 里,它是额外独立出来的。LinkedHashMap 通过继承 hashMap 中的 Entry<K,V>,并添加两个属性 Entry<K,V> before,after,和 header 结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序。

DisLruCache

DiskLruCache与LruCache原理相似,只是多了一个journal文件来做磁盘文件的管理,如下所示:
libcore.io.DiskLruCache
1
1
1

DIRTY 1517126350519
CLEAN 1517126350519 5325928
REMOVE 1517126350519

注:这里的缓存目录是应用的缓存目录/data/data/pckagename/cache,未root的手机可以通过以下命令进入到该目录中或者将该目录整体拷贝出来:
//进入/data/data/pckagename/cache目录
adb shell
run-as com.your.packagename
cp /data/data/com.your.packagename/

//将/data/data/pckagename目录拷贝出来
adb backup -noapk com.your.packagename

我们来分析下这个文件的内容:
第一行:libcore.io.DiskLruCache,固定字符串。 第二行:1,DiskLruCache源码版本号。 第三行:1,App的版本号,通过open()方法传入进去的。 第四行:1,每个key对应几个文件,一般为1. 第五行:空行 第六行及后续行:缓存操作记录。 第六行及后续行表示缓存操作记录,关于操作记录,我们需要了解以下三点:
DIRTY 表示一个entry正在被写入。写入分两种情况,如果成功会紧接着写入一行CLEAN的记录;如果失败,会增加一行REMOVE记录。注意单独只有DIRTY状态的记录是非法的。 当手动调用remove(key)方法的时候也会写入一条REMOVE记录。 READ就是说明有一次读取的记录。 CLEAN的后面还记录了文件的长度,注意可能会一个key对应多个文件,那么就会有多个数字。

8.关于Context的理解

9.安卓8.0、9.0、10.0适配

www.jianshu.com/p/a8fd3d1fa…
blog.csdn.net/guolin_blog…

Android5.0新特性

  • MaterialDesign设计风格
  • 支持64位ART虚拟机(5.0推出的ART虚拟机,在5.0之前都是Dalvik。他们的区别是: Dalvik,每次运行,字节码都需要通过即时编译器转换成机器码(JIT)。 ART,第一次安装应用的时候,字节码就会预先编译成机器码(AOT))
  • 通知详情可以用户自己设计

Android6.0新特性

  • 动态权限管理
  • 支持快速充电的切换
  • 支持文件夹拖拽应用
  • 相机新增专业模式

Android7.0新特性

  • 多窗口支持
  • V2签名
  • 增强的Java8语言模式
  • 夜间模式

Android8.0(O)新特性

  • 优化通知 通知渠道 (Notification Channel) 通知标志 休眠 通知超时 通知设置 通知清除
  • 画中画模式:清单中Activity设置android:supportsPictureInPicture
  • 后台限制
  • 自动填充框架
  • 系统优化
  • 等等优化很多

Android9.0(P):

(1)要求使用https
(2)Apache http客户端弃用
(3)要求创建一个前台服务需要请求FOREGROUND_SERVICE权限。
(4)刘海平适配。
(5)注意非 SDK 接口的限制。

Android10.0(Q):

(1)暗黑模式适配
(2)系统应用sd卡存储限制
(3)后台启动Activity限制
(4)读取手机状态权限限制

10.OkHttp

Okio优化思想和设计精髓:
(1)使用方便。
对比Java IO和Okio我们可以看出OKio使用更方便,支持链式调用,代码简洁、优美。缓存等功能对用户都是透明的,不需要了解底层结构也嫩方便实用。
(2)功能整合。Java IO进行不同的读写功能需要包裹各种装饰类,而Okio把各种读写操作都整合了起来,不需要串上一堆装饰类。
(3)cpu和内存的优化。数据容器采用循环链表实现,Segment通过分裂、合并、共享等操作避免了拷贝操作。SegmentPool会对暂时不用的Segment回收保存,避免频繁GC。看门狗在没任务的时候都处于休眠状态,不占用cpu。ByteString通过空间换时间,同时懒加载实现了cpu优化。
(4)功能强大。支持阻塞IO和非阻塞IO,提供了一系列的方便工具,如GZip的透明处理,对数据计算md5、sha1等都提供了支持,对数据校验非常方便。

11.Retrofit

使用的设计模式:建造这模式,简单工厂模式(platform),工厂模式(CallFactory),外观模式(接口),策略模式(CallAdapter)、适配器模式(call+platform)、代理模式(retrofit-create-InvocationHandler-invoke)、观察者模式(Callback)。

12.Glide

blog.csdn.net/xwh_1230/ar…

13.性能优化

juejin.im/post/5e7f12…
juejin.im/post/684490…

14.内存优化-LeakCanary原理

LeakCanary 中对内存泄漏检测的核心原理就是基于 WeakReference 和 ReferenceQueue 实现的。
(1)当一个 Activity 需要被回收时,就将其包装到一个 WeakReference 中,并且在 WeakReference 的构造器中传入自定义的 ReferenceQueue。
(2)然后给包装后的 WeakReference 做一个标记 Key,并且在一个强引用 Set 中添加相应的 Key 记录。
(3)最后主动触发 GC,遍历自定义 ReferenceQueue 中所有的记录,并根据获取的 Reference 对象将 Set 中的记录也删除。
经过上面 3 步之后,还保留在 Set 中的就是:应当被 GC 回收,但是实际还保留在内存中的对象,也就是发生泄漏了的对象。

15.热修复和插件化

(1)blog.csdn.net/qq_22090073… (2)mp.weixin.qq.com/s/zdoAgj4w6…

16.安全

三方:dev.bangcle.com/
Tea加密,解密算法:www.cnblogs.com/chevin/p/56…

17.kotlin协程

18.换肤

19.阅读器实现

20.架构设计

21.面试梳理(总览)

mp.weixin.qq.com/s/bCQCy8vkH…
mubu.com/doc/uRmziI6…
www.jianshu.com/p/b3c1b9c6d…

3595e42461b649b78766cc9574f5d6a3.png

三、设计模式

e812cf142371fed874cd5faaa797cc5d.webp