插图解释:Fresco 意大利语是“新鲜”,实际是指一种十分耐久的壁画,泛指在铺上灰泥的墙壁及天花板上绘画的画作,14-16世纪流行于意大利。
1、起因
书籍章节内容的加载(大量字符串)和图片加载总有着异曲同工的感觉,体积都不小,都可以用到三级缓存,所以看看能否临摹下Fresco
的框架。(Fresco官网)
2、代码分析
从SimpleDraweeView
源码看起,目标是找到加载图片的核心逻辑
先看代码调用路径,再概括对象功能,方法调用过程如下图,从右上角看起,触发图片加载的源头方法有两个:setimageURI
& onAttachedToWindow
,最终走到右下角的ImagePipeline
内:
关键对象:
PipelineDraweeControllerBuilder
: 只用于创建对象,创建了Controller
、一些Supplier
对象DraweeHolder
: 持有Controller
、Hierarchy
,持有核心代码,是核心代码层PipelineDraweeController
: 流程控制,其中submitRequest
中将一个订阅者传入了mDataSource
中,如下图:
这个订阅者最终会将数据转化为Drawable
,递交给GenericDraweeHierarchy
进行图片展示
再看进入ImagePipeline
之后,从下图左上角看起:
关键对象:
ProducerSequenceFactory
: 产生一个生产者序列,用于获取、加工数据,三级缓存就在其中ClosableProducerToDataSourceAdapter
: 顾名思义,适配器模式,将Producer
适配成DataSource
并返回对象,在这个对象创建之时即触发了Producer
的生产逻辑:
生产结束后,会触发我们一开始讲到的订阅者,将数据转换为Drawable
再递交给GenericDraweeHierarchy
进行图片展示
OK,加载逻辑到此大致走完,我们可以参考的就是Producer
的结构。
Producer
传递数据全是各种回调,每个步骤都可以使用异步处理,每个骤都可以随意拼接。
3、仿「Producer」「Consumer」结构
结构特点:
- 当前Producer持有下一个
Producer
,当前produceResults
失败则调用下一个produceResults
produceResults
传入一个Consumer
,这个Consumer
使用装饰模式
,每层Producer
都有自己的装饰,用于层层回调
章节内容加载代码的改造(代码已简化):
定义运行上下文:
// 某本书的某个章节
class DataContext(
val book: Book,
val chapter: Chapter
) {
companion object {
const val PRODUCER_MEM = 0
const val PRODUCER_DISK = 1
const val PRODUCER_NET = 2
}
var readCache: Boolean = true // 是否读缓存
var writeCache: Boolean = true //是否写缓存
var useNetwork: Boolean = true //是否从网络获取
var fromProducer: Int = PRODUCER_MEM //数据由哪个生产者产生
}
定义接口:
interface Producer<Data> {
fun produceData(context: DataContext, consumer: Consumer<Data>)
}
interface Consumer<Data> {
fun consumeData(context: DataContext, data: Data?)
}
内存生产者:
class MemoryProducer(
private val nextProducer: Producer<ChapterContent>? = null
) : Producer<ChapterContent> {
companion object {
private val lruCache = LruCache<String, ChapterContent>(MAX_CACHE_CHAPTER * MAX_CACHE_BOOK)
/**
* 生产:从内存中取
*/
fun produce(bookId: Long, chapterId: Long): ChapterContent? {
return lruCache.get(cacheKey(bookId, chapterId))
}
/**
* 消费:内存缓存
*/
fun consume(bookId: Long, chapterId: Long, chapterContent: ChapterContent) {
lruCache.put(cacheKey(bookId, chapterId), chapterContent)
}
}
override fun produceData(context: DataContext, consumer: Consumer<ChapterContent>) {
if (context.readCache) {
produce(
context.book.bookId,
context.chapter.chapterId
)?.also {
// 从内存中获取到内容
IKLog.d(TAG, "Memory Produce: ${context.book.bookId}, ${context.chapter.chapterId}")
context.fromProducer = DataContext.PRODUCER_MEM
consumer.consumeData(context, it)
return
}
}
// 未获取到内容,下一个生产者,并包装一层「当前层」的消费逻辑
nextProducer?.produceData(context, WrapConsumer(consumer))
}
private class WrapConsumer(
private val nextConsumer: Consumer<ChapterContent>
) : Consumer<ChapterContent> {
// 内存层的消费逻辑
override fun consumeData(context: DataContext, data: ChapterContent?) {
if (context.writeCache && data != null) {
IKLog.d(TAG, "Memory Consume: ${context.book.bookId}, ${context.chapter.chapterId}")
consume(context.book.bookId, context.chapter.chapterId, data)
}
nextConsumer.consumeData(context, data)
}
}
}
磁盘生产者:
class DiskProducer(
private val nextProducer: Producer<ChapterContent>? = null
) : Producer<ChapterContent> {
companion object {
/**
* 生产:从磁盘中取
*/
fun produce(chapter: Chapter): ChapterContent? {...}
/**
* 消费:存入磁盘
*/
fun consume(chapter: Chapter, chapterContent: ChapterContent) {...}
}
override fun produceData(context: DataContext, consumer: Consumer<ChapterContent>) {
if (context.readCache) {
produce(context.chapter)?.also {
// 从磁盘中获取到内容
IKLog.d(TAG, "Disk Produce: ${context.book.bookId}, ${context.chapter.chapterId}")
context.fromProducer = DataContext.PRODUCER_DISK
consumer.consumeData(context, it)
return
}
}
// 未获取到内容,下一个生产者,并包装一层「当前层」的消费逻辑
nextProducer?.produceData(context, WrapConsumer(consumer))
}
private class WrapConsumer(
private val nextConsumer: Consumer<ChapterContent>
) : Consumer<ChapterContent> {
// 磁盘层的消费逻辑
override fun consumeData(context: DataContext, data: ChapterContent?) {
if (context.writeCache && data != null) {
IKLog.d(TAG, "Disk Consume: ${context.book.bookId}, ${context.chapter.chapterId}")
consume(context.chapter, data)
}
nextConsumer.consumeData(context, data)
}
}
}
网络生产者:
class NetworkProducer : Producer<ChapterContent> {
companion object {
/**
* 生产:从网络获取,同步返回(网络是最后一层)
*/
fun produce(bookId: Long, chapterId: Long, contentUrl: String?): ChapterContent? {
...
}
}
override fun produceData(context: DataContext, consumer: Consumer<ChapterContent>) {
if (context.useNetwork) {
produce(
context.book.bookId,
context.chapter.chapterId,
context.chapter.encUrl
)?.also {
// 从网络获取到内容,直接消费
consumeProduce(context, consumer, it)
return
}
}
// 获取失败,或者不允许使用网络层,直接调用上层的消费方法
consumer.consumeData(context, null)
}
// 最后一层不定义包装类,因为不用传入下一层了,直接调用当前层的消费逻辑,再层层回调
private fun consumeProduce(
context: DataContext,
consumer: Consumer<ChapterContent>,
chapterContent: ChapterContent
) {
IKLog.d(TAG, "Network Produce: ${context.book.bookId}, ${context.chapter.chapterId}")
// 写缓存控制:某些情况不进行缓存,合并一下原本的写缓存值
context.writeCache = ... && context.writeCache
context.fromProducer = DataContext.PRODUCER_NET
consumer.consumeData(context, chapterContent)
}
}
获取章节内容入口:
// 三级套娃
private val normalSequence = MemoryProducer(DiskProducer(NetworkProducer()))
/**
* 获取单章内容
*/
fun fetchChapterContent(
book: Book,
chapter: Chapter,
success: (ChapterContent, Int) -> Unit // 成功回调
) {
// 构造加载数据上下文,设置是否需要缓存
val context = DataContext(book, chapter)
context.readCache = ...
context.writeCache = ...
// 获取数据,传入consumer对象
normalSequence.produceData(context, FetchConsumer(success))
}
private class FetchConsumer(
private val success: (ChapterContent?, Int) -> Unit,
) : Consumer<ChapterContent> {
override fun consumeData(context: DataContext, data: ChapterContent?) {
success(data, context.fetchType) // 回调内容 & 获取方式
}
}
PS:这里异步执行的核心逻辑是:将代码打包成对象,随方法一路传递,在异步执行结束时调用该对象中的方法。适当的结构可能会带来更多的代码,但是极大地增加了可读性。
4、三级缓存中的两个细节设计
(1)防止内存抖动(MemoryChunkPool)
在NetworkFetchProducer
对象内,网络加载数据后,读取的Response
数据流会使用MemoryPooledByteBufferOutputStream
保存,该类使用MemoryChunkPool
,它可以回收复用同样大小的内存空间,防止内存抖动
(malloc
分配一块指定大小的内存,并返回指向这块内存开始地址的指针,这块内存在native heap
中)
可以复用到章节内容下载上,尤其是批量下载
(2)将文件写入变为原子操作(BufferedDiskCache)
在DiskCacheWriteProducer
内,将数据写入磁盘时使用了BufferedDiskCache
,BufferedDiskCache
内操作文件的接口是FileCache
,实现类实现是DiskStorageCache
:
看到DiskStorageCache
的插入文件方法
看到这个注释有个疑问,为什么插入文件要先写一个缓存文件,再进行move?并且这样可以执行很多的并行写入?
实际上这种操作将写入文件变成了一个 原子操作
,使得写入文件允许并发写入(复习一下并发编程三大特性:原子性、可见性、有序性)。
使用临时文件进行写入的好处:
- 将写入文件操作变为原子操作。
- 多线程/进程读文件时,总是可以读取到完整的文件(不必担心写入异常中断,或者写入速度不够快而造成的文件不完整)
- 临时文件放在系统的临时目录,方便清理,不必担心残缺的垃圾文件。