在学习Android过程中,会使用到很多开源框架,而使用时对框架的实现方式应有一定的了解,这个过程最好的方式就是阅读源码,毕竟源码说明一切。但开源框架很多,不时会有新的出现,而且对一些通用框架来说,一般代码量非常庞大,所以正确的阅读姿势很重要。一般来说有下面几个要点:

  • 明确阅读源码的目的是对整体有个了解而非细节,对开源框架来说就是先找这个框架的特点,其他相同业务框架相比的优点和不足,这也是选择的考虑的重点;
  • 厘清一个框架的核心点和辅助点;
  • 最重要的是总结相似框架的架构模式与习惯,慢慢的就会有一套自己的阅读套路。

这里主要通过常用的网络请求库OkHttp,图片加载库Picasso,事件总线库EventBus等对上面提到的几点进行解读。

常用的架构模式

下图是通过阅读一些框架源码的过程总结出来的一些通用组件(或开源库)的流程图,并对每个过程进行简要分析,并不是所有的开源框架都会严格按照这个流程图里的执行步骤,但一般都会有类似的组成部分,只是个别部分的过程有些差别,一般都可以类比到这个框架中。你可以尝试用这个框架创建自己的通用库,这也是自己实现的事件总线框架Router所遵循的框架。

通用开源框架的源码解读套路

初始化

开源库使用前一般都会做一些初始化的配置工作,一般都是一些类的初始化,比如调度器的初始化,线程池的创建,缓存的存储位置、大小配置,网络库来说还有是否支持的协议,http1.1,http2,spdy3等,对图片加载库,加载网络图片使用的网络配置,比如Picasso就是将加载网络配置为OkHttp等等。

一般会有一些默认配置,同时提供Builder模式让使用者可自行配置。

对EventBus来说,类的注册也是属于初始化这个过程,同时又有些不同,因为对OkHttp和Picasso来说初始化后一般在整个应用的生命周期里配置不在变化,而EventBus在注册和取消注册的过程中不断变化。把这个过程归位初始化是因为请求发生时,状态是固定的。

新的请求

也就是请求数据等待返回的过程,不是狭义的网络请求中的Request。

OkHttp中

1
2
3
4
5
6
7
//同步
Response = Call.execute();
//异步
Call.enqueue(new Callback() {
...
});

Call.execute和Call.enqueue是请求。

Picasso中

1
Picasso.with(this).load(url).into(iv);

Picasso的load(url)是创建请求对象,into(iv)的过程才是真正的发出请求。

EventBus中

1
EventBus.post(event);

调度器

请求建立后,就需要调度器来获取数据了,调度器是框架的核心。一般管理着缓存的匹配,请求的再次封装,请求队列的维护,任务执行等过程。

缓存

缓存的作用避免重复的请求,提高响应的速度,尤其对需要进行外部资源加载到内存的框架来说更加重要。

常见的缓存指内存缓存(Memory Cache),文件缓存(Disk Cache也叫File Cache)和服务器缓存(Server Cache针对网络请求),数据库缓存等。

由于不同的业务需求,不同的框架可能需要不同的缓存策略,如OkHttp等网络框架和Picasso等图片加载框架中对缓存的依赖成都很高,并且占用较大的内存,所以需要借助一些缓存算法来实现。

OkHttp中可以通过Cache-Control HTTP 首部字段允许缓存,但是默认情况下,OkHttp并不会缓存这些响应消息,为了在文件系统中开启响应缓存,需要配置一个Cache 实例,然后把它传递给 OkHttpClient 实例的 setCache 方法。你必须用一个表示目录的 File 对象和最大字节数(一般为10M)来实例化Cache对象。并通过DiskLruCache来控制缓存的添加与移除。使用了服务器缓存和磁盘缓存。

Picasso中使用LruCache来控制内存缓存,默认大小是应用最大RAM的15%,Picasso没有自定义本地缓存的接口,默认使用 http 的本地缓存,文件缓存交给OkHtt去实现,这样的好处是可以通过请求 Response Header 中的 Cache-Control控制图片的过期时间。

EventBus中对缓存的需求是Sticky事件的缓存,用Map来维护,只需要内存缓存。

业务逻辑任务

前几个部分虽然不同的框架有不同的方式,但很多东西的原理是想通的,比如单例获取,Builder模式配置,LRU相关缓存策略缓存配置方式等。核心逻辑才是不同类别库变化最大的地方。也是同类别类库进行选择的判断依据。

对网络库来说其主要逻辑是通过DNS解析URL,获取URL对应的主机IP和端口(一般为80端口),然后通过Socket的连接,成功后开始从服务端获取数据流,这个过程一般被封装在HttpClient、HttpURLConnection中,而OkHttp的底层并未用到这两个客户端,而是自己实现了这些部分逻辑,其中用到Okio来实现数据流的读取,它补充了java.io和java.nio的不足,以便能够更加方便,快速的访问、存储和处理你的数据。

另一个主要部分是根据Http协议,把请求包装成Http请求包,格式如下

请求包

例如请求silencedut.com,请求包如下:

Get / HTTP/1.1

Cache-Control: max-age=0
Accept-Encoding: gzip
Connection: keep-alive
Host: silencedut.com
...

请求体为空

返回的相应体为:

相应包

类似这样

HTTP/1.1 200 OK

Date: Sat, 10 Sep 2016 21:20:55 GMT
Server: nginx
Content-Type: text/html;charset=utf-8
...

相应包体是html页面

所以对网络库来说就是获取数据流,然后根据不同的协议进行解析,如果相应包体是Json类型,需要转换成相应的类结构,需要再依赖一些Json解析库如Gson,fastJson等,实现也很简单,Retrofit和Volley里就是这么做的。

对图片库来说,对于网络加载图片,这也是最常使用的,其原理是和上面的网络请求类似,只不过将得到数据流之后需要转化成Bitmap类型.

1
2
InputStream is = conn.getInputStream();
bitmap = BitmapFactory.decodeStream(is);

对本地图片同理

FileInputStream fis = new FileInputStream(url);
bitmap = BitmapFactory.decodeStream(fis);

而对于资源内的图片,直接可以加载。

再就是利用上面提到的缓存LRU算法,对数据进行缓存。避免重复的获取和解析。

对事件总线库就是对之前注册的方法进行调用处理,在Router中需要动态代理对象的创建过程。

任务的执行过程

任务的执行一般是两种情况,同步的和异步的。

在当前线程执行,也就是同步任务,这个过程可能是阻塞性的,比如OkHttp中的

Response = Call.execute();

直接在当前线程完成,EventBus和Router中添加注解为POSTING 时,也是在当前线程完成,不需要借助线程池。

在非当前线程执行,一般是借助线程池来完成,关于线程池的细节和使用可以参考从使用到原理学习Java线程池。因为常用框架中会有大量的请求进行,线程池可以避免线程的重复创建,可以重复利用线程。这种情况下,需要阻塞队列来管理请求的添加与执行顺序,阻塞队列可以用JDK提供的API中的BloackingQueue,也可以自己实现和维护一个队列。对于在线程池执行的任务,从从使用到原理学习Java线程池可以看到,一定是个Runnable,一般是将业务逻辑任务封装成Runnable或者在Runnable处理具体的逻辑。

结果分发

根据执行任务的情况,如果是在当前线程同步获取,可以直接得到结果,有些情况下可能是阻塞的。

线程池里,一般是使用FutureTask(Callable和Future、FutureTask的使用)来获取线程池执行的结果。

在Android中任务执行的结果一般需要传递到UI线程,而Android中与UI线程的交互过程基本上就是通过Handler了。

并发控制和内存泄露预防

由于涉及到多个线程的读取过程,所以需要相应的并发控制,常用的并发工具有synchronized,volatile,ConcurrentHashMap,CopyOnArrayList,Aomtic等,详细的介绍可参考并发的学习与使用.
由于Cache的管理过程,所以需要对不需要的引用及时去除,如及时取消注册释放内存,或者使用弱引用等。为了避免Cache过大导致内存溢出,也需要一些缓存算法LRU等。

总结

上面提到的这些就是开源框架常用的套路,在上面提到的几个开源库里都能找到对应的实现,在一些其他类似的库里应该也能找到。可能需要多个步骤组合而成,夹杂相应的设计模式,所以需要有一定的知识积累,这里只是这些开源框架的大概框架,为了方便快速的对框架有个全面的认识,如果想知道更多的细节可以或者想学习一些设计技巧可以去好好研究一下相应的代码。当然你也可以用类似的框架实现一个常用的开源库,就会发现可能也没想象中的那么难。