阅读 1614

第三方库原理复习笔记(一)

生活总是让我们遍体鳞伤,但是后来,那些受伤的地方一定会变成我们最强壮的地方。

本文目录

  • OkHttp
  • LeakCanary

OkHttp

网络库的发展史:

  • 一开始使用的是HttpClient,但是目前已经被废弃掉了。
  • Volley是Google官方2013年出的一套小而巧的异步请求库,而且 Volley 里面也封装了 ImageLoader ,所以如果你愿意你甚至不需要使用图片加载框架,不过这块功能没有一些专门的图片加载框架强大。 Volley 也有缺陷,比如不支持 post 大数据,因为线程数只有4个,所以不适合上传文件。不过 Volley 设计的初衷本身也就是为频繁的、数据量小的网络请求而生!
  • OkHttp是Square公司开发的网络请求框架,用于代替HttpUrlConnection和HttpClient,它具有以下优点:
    • OkHttp 提供了对最新的 HTTP 协议版本 HTTP/2 和 SPDY 的支持,这使得对同一个主机发出的所有请求都可以共享相同的套接字连接。
    • 如果 HTTP/2 和 SPDY 不可用,OkHttp 会使用连接池来复用连接以提高效率。
    • OkHttp 提供了对 GZIP 的默认支持来降低传输内容的大小
    • OkHttp 也提供了对 HTTP 响应的缓存机制,可以避免不必要的网络请求。
    • 当网络出现问题时,OkHttp 会自动重试一个主机的多个 IP 地址

OkHttp可以使用在:get、post请求、上传文件、上传表单等场景。

Q:OkHttp是如何复用连接的?

  • Http1.1:Keep-Alive机制
  • Http2: 多路复用

OkHttp实现复用,依赖的原则是Http协议。在Http1.1中规定,当设置Keep-Alive为true的时候,可以复用同一条连接;在Http2.0中则默认支持多路复用连接。

但是只是协议支持还不够,协议只是规定允许这样做,真正的实现还需要双端负责。在OkHttp中,是通过TCP连接池来进行管理连接,实现连接复用的。

要实现TCP连接复用,需要解决以下问题:

  • 如何判断可以复用这条连接?
  • 如何找到这条连接?
  • 如何清理过期不用的连接?

Q:如何清理连接流程:

  • 找到没有被引用的连接
  • 找到空闲最久的连接
  • 若空闲最久的连接 空闲的时间超过了设置的KeepAliveDurations(不是Keep-ALive锁设定的时间),,或者空闲连接数超过了所设定的 maxIdleConnections,清理该连接(移除并关闭socket),并返回 0 表示立即继续清理
  • 如果还没有超过设置的KeepAliveDurations,那么返回剩余时间,等到下次超时再清理
  • 如果当前连接都是正在使用的,返回keep-alive所设定的时间
  • 若当前没有连接,则将 cleanupRunning 置为 false 停止清理

在 OkHttp 中,将空闲连接的最长存活时间设定为了 5 分钟,并且将最大空闲连接数设置为了 5

这一块的逻辑放在RealConnectionPoolcleanup方法中,实现是方式是:开启一条线程进行不断轮询。

在这一块设计时,对于每一条Connection引用的Call,使用的是WeakReference存储,这种设计有点像 JVM 中的引用计数法 + 标记清除,实际上就是 OkHttp 仿照 JVM 的垃圾回收设计了这样一种类似引用计数法的方式来统计一个连接是否是空闲连接,同时采用标记清除法对空闲且不满足设定的规则的连接进行清除。

  internal class CallReference(
    referent: RealCall,
    /**
     * Captures the stack trace at the time the Call is executed or enqueued. This is helpful for
     * identifying the origin of connection leaks.
     */

    val callStackTrace: Any?
  ) : WeakReference<RealCall>(referent)
复制代码

Q:如何判断可以复用这条连接?

相关代码如下:

这一段的逻辑是:

  • 当需要进行多路费用且当前的连接不是 HTTP/2 连接时,则放弃当前连接
  • 当当前连接不能用于为 address 分配 stream(比如:超过连接数、host不同等),则放弃当前连接。
  • 两者都不满足,则获取该连接,并设置到 calls 中。

如果没有找到可以用的连接,那会进行创建。

Q:如何找到可以复用的连接?

这里要注意的是,找到findConnection中一共会有三次尝试从连接池中获取连接: 第一次: 这次是尝试从已经解析过的路路由连接池中获取连接,因此route设置为null。

第二次: 第二次由于是在无法找到对应的连接,在进行了路由选择的条件下进行的,因此将 route 设置为了 null。

第三次: 最后一次尝试从连接池获取连接之所以需要将 requireMultiplexed 设置为 true,因为这次只有可能是在多个请求并行进行的情况下才有可能发生,这种情况只有 HTTP/2 的连接才有可能发生。

Q:什么时候把连接加入到连接池的?

把连接放入到连接池这块的代码在RealConnectionPool.put方法: 主要是在ExchangeFinder的findConnection方法中,如果找到了一条连接,就放入连接池。

Q:你从这个库中学到什么有价值的或者说可借鉴的设计思想?

这一块应该被说烂了吧,就是责任链模式。

责任链模式适用于一个事件需要经过多个对象处理是一个挺常见的场景,譬如采购审批流程,请假流程,软件开发中的异常处理流程。

Android中事件分发也是这样的。

优点:(要从设计原则来讲)

  • 降低耦合 -》客户端不需要知道如何处理,只需要知道会被处理
  • 简化连接 -》请求处理对象不需要持有所有候选处理者,只需要持有下一个处理者
  • 符合开闭原则 -》新增一个请求处理者,无需修改原有代码

缺点:

  • 由于职责链处理不当,造成循环调用,导致系统陷入死循环
  • 职责链比较长,对系统性能有影响
  • 一个请求可能因为职责链没有正确配置得不到处理

Q:网络请求缓存处理,okhttp如何处理网络缓存的?

OkHttp的网络缓存是根据Http的缓存策略实现的。

OkHttp的整体缓存策略是:

  • 第一次拿到响应后根据头部信息决定是否缓存
  • 下次请求时判断本地是否存在缓存,是否需要使用使用
  • 如果缓存失效或需要对比缓存,则发出网络请求,否则就使用本地缓存。

Http的缓存策略如下:

完整流程是:

OkHttp内部使用Okio来实现缓存文件的读写。

缓存文件分为CleanFiles和DirtyFiles,CleanFiles用于读,DirtyFiles用于写,他们都是数组,长度为2,表示两个文件,即缓存的请求头和请求体;同时记录了缓存的操作日志,记录在journalFile中。

Q:自己去设计网络请求框架,怎么做?

  • 构建请求,包括:URL、请求方法、请求头、编码、请求体、编码格式、超时时间、代理端口、代理主机等
  • 请求分发,实际上就是一个线程池
  • 准备开始连接服务器,获取服务器地址,https还需要考虑签名证书,双向SSL验证等问题
  • 得到服务器的回调,大文件传输问题、线程切换问题

Q:从网络加载一个10M的图片,说下注意事项?

从四个方面去考虑,分别是:

  • 协议:采用Https、流传输、加入断点续传 + 拥塞控制 + 滑动窗口
  • 服务端优化:降级、容灾
  • 本地存储:IO(字节) + 缓存
  • 展示:如何防止OOM,要用Subsampling Scale Image View进行分块记载
  • 回收

Q:addInterceptor与addNetworkInterceptor的区别

二者通常的叫法为应用拦截器和网络拦截器,从整个责任链路来看,应用拦截器是最先执行的拦截器,也就是用户自己设置request属性后的原始请求,而网络拦截器位于ConnectInterceptor和CallServerInterceptor之间,此时网络链路已经准备好,只等待发送请求数据。

  • 首先,应用拦截器在RetryAndFollowUpInterceptor和CacheInterceptor之前,所以一旦发生错误重试或者网络重定向,网络拦截器可能执行多次,因为相当于进行了二次请求,但是应用拦截器永远只会触发一次。另外如果在CacheInterceptor中命中了缓存就不需要走网络请求了,因此会存在短路网络拦截器的情况。

  • 其次,如上文提到除了CallServerInterceptor,每个拦截器都应该至少调用一次realChain.proceed方法。实际上在应用拦截器这层可以多次调用proceed方法(本地异常重试)或者不调用proceed方法(中断),但是网络拦截器这层连接已经准备好,可且仅可调用一次proceed方法。

  • 最后,从使用场景看,应用拦截器因为只会调用一次,通常用于统计客户端的网络请求发起情况;而网络拦截器一次调用代表了一定会发起一次网络通信,因此通常可用于统计网络链路上传输的数据。

Q:让你实现LruCache,你会如何实现?

实现思路:

1、需要用到的数据结构

  • 用一个链表保存数据的添加顺序,队尾是最近使用的,对头是最早使用的(优先移除),为了提高效率,可以用两个对象分为保存头尾节点
  • 用一个Map保存当前保存的所有数据,作用是便于判断队列中是否有这个数据ji

2、保存(put)的实现设计

  • 根据key从map中查找是否存在这个对象
  • 如果存在,那么先从链表中移除这个对象,然后把对象移动到队尾
  • 如果不存在,直接添加到队尾

3、获取(get)的实现设计

  • 根据key从map中查找是否存在这个对象
  • 如果存在,那么先从链表中移除这个对象,然后把对象移动到队尾,返回该对象
  • 否则,返回null

Q:OkHttp调度设计优雅?

1、采用Dispacher作为调度,与线程池配合实现了高并发,低阻塞的的运行

2、采用Deque作为集合,按照入队的顺序先进先出

3、最精彩的就是在try/catch/finally中调用finished函数,可以主动控制队列的移动。避免了使用锁而wait/notify操作。 ??不懂

Q:你们用的 okhttp ?那你有没有做过一些网络优化呢?比如弱网环境。

移动端的网络优化,主要分为以下三个方面:

  • 速度 在网络正常或者良好的时候,怎样更好地利用带宽,进一步提升网络请求的速度。

  • 弱网络 移动端网络复杂多变,在出现网络连接不稳定的时候,怎样最大程度保证网络的连贯性。

  • 安全 网路安全不容忽视,怎样有效防止被第三方劫持、窃听甚至篡改。

一个网络请求的整个过程如下:

因此,我们得到的优化思路有:

  • 合并请求
    • 合并网络请求,减少请求次数
  • 设置网络缓存
  • 减少传输量
    • Protocol Buffer 是 Google 开发的一种数据交换的格式,相较于目前常用的 JSON,数据量更小,意味着传输速度也更快。
  • ip直连
    • DNS 解析的失败率占联网失败中很大一种,而且首次域名解析一般需要几百毫秒。针对此,我们可以不用域名,采用 IP 直连省去 DNS 解析过程,节省这部分时间。

Q:NIO性能为什么要比IO性能好?

Okio核心竞争力为,增强了流于流之间的互动,使得当数据从一个缓冲区移动到另一个缓冲区时,可以不经过copy能达到。

  • 1 速度快

okio采用了segment机制进行内存共享,极大减少copy操作带来的时间消耗,加快了读写速度 okio引入ByteString使其在byte[]与String之间转换速度非常快(ByteString内部以两种变量记录了同个数据byte[] data; transient String utf8;),空间换时间

  • 2 稳定

okio提供了超时机制,不仅在IO操作上加上超时的判定,包括close,flush之类的方法中都有超时机制

  • 3 内存消耗小

虽然okio在byteString采用空间换时间,但是对内存也做极致优化,总体还是极大提高了性能 okio的segement机制进行内存复用,上传大文件时完全不用考虑OOM

LeakCanary

Q: leakcanary 的原理,哪些对象可以用来做 gc-root

好,你说你主要擅长应用层开发,那 Java 层的内存泄漏怎么检测,我说我们用的 leakcanary,让我说说原理,说完原理又问我是不是所有对象泄漏 leakcanary 都能检测得到,他的引用链是怎么管理的?后面问到你刚说弱引用对象在 gc 的时候会被释放,那什么时候不会被释放?我这时懵逼了,其实就是有内存泄漏的时候不会被释放,我当时脑子短路了居然没反应过来。

根据《深入理解Java虚拟机》的描述,可以用作GCRoot的地方如下:

  • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中常量引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(native)引用的对象

LeakCanary是用来检测内存泄漏的,内存泄漏指的是,当一个对象在程序中已经不再使用了,但是还是会被其他对象强引用持有,这就叫做内存泄漏。内存泄漏严重的后果就是会造成OOM。

那么,LeakCanary是如何检测到内存泄漏的呢? LeakCanary解决这个问题的思路是:如果一个对象被弱引用(WeakReference),当要被回收掉的时候,会被加入到ReferenceQueue

扩展:四种引用

  • 强引用:永远不会被回收
  • 软引用:在系统要发生OOM之前回收
  • 弱引用:只要有GC,不管内存是否足够,都会被回收
  • 虚引用:在这个对象被收集器回收时收到一个系统通知,需要搭配ReferenceQueue使用

有了这个基本思路,接下来只要解决以下问题,一个检测内存泄漏的方案就形成了:

  • 如何收集到所有的Activity
  • 何时将Activity放入到弱引用对象中
  • 何时去检测ReferenceQueue
  • 如何分析出引用链

通过阅读源码,可以得出的答案是:

  • 通过registerActivityLifecycleCallbacks()方法注册 Activity 生命周期回调
  • onDestroy()回调中,注册IdleHandler,当收到回调的时候,将每个Activity对象添加到观察列表,这里有两个重要的变量,一个是保存所有Activity对应key的Set,一个收集被回收的Activity的queue
  • 当检测到被回收时,会根据key把Activity对应的key从set中移除
  • 强制GC后再次检查
  • 这时set集合中存在的就是发生泄漏的Activity
  • 调用Debug.dumpHprofData生成堆转储文件
  • 调用HAHA(v1.x版本)进行分析

LeakCanary已经升级到了v2版本了,有一些东西也发生了改变:

  • 支持 fragment泄漏监听,支持 androidx
  • 当泄露引用到达 5 个时才会发起 heap dump
  • 全新的 heap parser,减少 90% 内存占用,提升 6 倍速度

不过都是一些优化的东西,总体的解决思想是不变的。

LeakCanary2升级后有一个小小的点挺取巧的,那就是利用ContentProvider进行初始化,减去了在App端配置的手续。 WorkManager也是这么干的,不过由于LC只用于debug阶段,所有也不影响。 看代码:

internal class LeakSentryInstaller : ContentProvider() {

  override fun onCreate(): Boolean {
    CanaryLog.logger = DefaultCanaryLog()
    val application = context!!.applicationContext as Application
    InternalLeakSentry.install(application)  
    //骚操作在这里,利用系统自动调用CP的onCreate方法来做初始化
    return true
  }

  override fun query(
    uri: Uri,
    strings: Array<String>?,
    s: String?,
    strings1: Array<String>?,
    s1: String?
  )
: Cursor? 
{
    return null
  }

  override fun getType(uri: Uri): String? {
    return null
  }

  override fun insert(
    uri: Uri,
    contentValues: ContentValues?
  )
: Uri? 
{
    return null
  }

  override fun delete(
    uri: Uri,
    s: String?,
    strings: Array<String>?
  )
: Int 
{
    return 0
  }

  override fun update(
    uri: Uri,
    contentValues: ContentValues?,
    s: String?,
    strings: Array<String>?
  )
: Int 
{
    return 0
  }
}

复制代码

Q:为什么 LeakCanary 要在 Activity onDestroy 之后调用 RefWatcher.watch() 开始监测内存泄露?

从 ActivityThread 的角度看,Activity 就是一个对象,按照 GC Root,Activity 是被 ActivityThread 给引用着的。为什么要在 onDestroy 之后开始检测,因为这个时候 Activity 和 ActivityThread 的引用断开了,在 ActivityThread.performDestroyActivity() 中从 mActivities 中移除了当前 Activity 对象。

Q:LeakCanary和Matrix中ResourcePlugin内存泄漏检测的区别?

场景不同:

  • LeakCanary只适用于线下
  • ResourcePlugin也适用于线上

鉴定内存泄漏原理不同:

  • LeakCanary是通过判断ReferenceQueue上是否窜在某个对象是否被回收
  • ResourcePlugin是通过WeakReference.get()来判断某个对象是否被回收,可以因延迟造成的误判

Hprof文件:

  • LeakCanary:没有提供
  • ResourcePlugin:支持Hprof文件剪裁和上传

Hprof分析:

  • LeakCanary:自带分析
  • ResourcePlugin:除了泄漏的分析,额外支持冗余Bitmap最短引用链分析

参考文章