尉涛/广州技术
一、起因
随着产品设计的不断深入,对代码的健壮性要求越来越高,同时也希望能从架构方面入手去修正一些问题。
「阅读器章节内容加载」和「图片加载」总有着异曲同工的感觉,都需要三级缓存,都有一定的体积。
所以希望能从Fresco源码中学到些框架设计,看看能否临摹下Fresco的代码。
二、Fresco源码分析
我们从「SimpleDraweeView」看起,看一个图片是如何实现加载的
先看代码调用路径,再概括对象功能
整条调用路径分两部分来看,「进入ImagePipeline前」和「进入ImagePipeline后」,先看前者
调用入口有两个:「setImageURI」和「onAttachedToWindow」
(1)先看「setImageURI」的左边
调用「PipelineDraweeControllerBuilder」相关方法并创建了两个重要对象:「Controller」& 「Supplier」
Controller:由「obtainController」方法返回。
Supplier:由「getDataSourceSupplierForRequest」方法返回,并作为「Controller」的初始参数,被「Controller」持有。
「Supplier」类似Kotlin中的「lazy」,一种延迟执行的机制,这里的「Supplier」会延迟构造一个「DataSource」对象。
「DataSource」由「ImagePipeline」构造返回,最终我们可以用「DataSource」的「getResult」方法获取图片数据。
总结一下:
「PipelineDraweeControllerBuilder」只负责构造,构造关键对象
(2)再看右半部分:
「setImageURI」和「onAttachedToWindow」分别代表两个条件(加载路径 & View Attach)
「DraweeHolder」持有前面创建的「Controller」,判断条件满足后使用「Controller」调用「Supplier」的get方法,触发图片的加载流程
总结一下:
「DraweeHolder」是DraweeView的核心代码层
其中「PipelineDraweeController」负责流程控制
(「DraweeHolder」还持有了「Hierarchy」,另外一个控制器)
(3)在看下Controller中的「submitRequest」方法
PipelineDraweeController 负责流程控制,其中「submitRequest」中将一个订阅者传入了「mDataSource」中:
这个订阅者最终会将数据转化为Drawable,递交给「GenericDraweeHierarchy」进行图片展示
顾名思义,「Hierarchy」负责图片层级控制(占位图、加载图片的层级关系)
进入ImagePipeline后逻辑其实非常简单,关键的也是两步
(1)构造「Producer」序列
顶部灰色方框内的逻辑我们基本不用关心,只用知道:
「ProducerSequenceFactory」生成了一个「Producer」队列,用于获取、加工数据,三级缓存就在其中。
图片的核心加载逻辑就在各种「Producer」中,可以看到一共有十几种「Producer」,这里面不光有缓存,还有各种数据转换等操作
这个「Producer」序列会传入接下来构造的「DataSource」中。
(2)创建「DataSource」对象
准确来说是创建一个实现「DataSource」接口的对象
这里的「CloseableProducerToDataSourceAdapter」使用了「适配器模式」,将「Producer」适配为「DataSource」
在这个对象创建之时即触发了「Producer」序列中首个「Producer」的生产逻辑:
「Producer」序列生产结束后,会触发我们前面讲到的订阅者,将数据转换为Drawable再递交给「GenericDraweeHierarchy」进行图片展示。
生产结束后,也可以使用「DataSource」的「getResult」主动获取结果数据。
「Producer」序列结构特点:
(1)当前「Producer」持有下一个「Producer」,当前「produceResults」失败则调用下一个「produceResults」。
(2)「produceResults」方法会接收上一个「Producer」构造的「Consumer」,这个「Consumer」使用装饰模式,打包了上一个「Producer」的回调逻辑
当前「Producer」生产成功则调用这个「Consumer」进行回调;每层「Producer」都有自己的装饰,用于层层回调。
OK,加载逻辑到此大致走完,过程总结下:
1、设置条件:setURI + attachView,构造Controller、Supplier
2、核心代码层处理逻辑,Controller控制流程,调用Supplier的get
3、ImagePipeline产生ProducerSequence,并生产出DataSource返回给Supplier的get,同时触发ProducerSequence的生产流程
4、回调 Hierarchy 展示图片
我们可以参考的就是「Producer」序列这个结构,它的每个步骤都可以使用异步处理,每个步骤都可以随意拼接。
(异步处理的根本在于将代码打包到对象中,在某个时机调用这个对象的相关方法,每个「Producer」都会构造一个「Consumer」交给下一个「Producer」用于回调)
三、仿「Producer」「Consumer」结构
对OwONovel的下载章节内容进行了改造:
上下文包含书籍信息与章节信息,以及流程控制的一些参数
class DataContext(
val book: Book,
val chapter: BookChapter
) {
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 //数据由哪个生产者产生
}
2、获取章节内容入口
可以看到我们声明的序列,Memory → Disk → Network
以及构造Context,对加载流程进行控制,有时不需要读缓存,有时不需要写缓存,都可以自由控制
private val normalSequence = MemoryProducer(DiskProducer(NetworkProducer()))
/**
* 获取单章内容
*/
fun fetchChapterContent(
book: Book,
chapter: BookChapter,
success: (ChapterContent, Boolean) -> Unit
) {
val time = BookDiskHelp.getChapterContentSaveTime(chapter.getFileName())
// 更新时间大于保存时间,刷新数据,不用读缓存,要写缓存
val refresh = chapter.updateTimestamp > time
// 限时免费,不要读写缓存
val freeForLimit = book.freeForLimit == 1
// 构造加载数据上下文
val context = DataContext(book, chapter)
context.readCache = !freeForLimit && !refresh
context.writeCache = !freeForLimit
// 获取数据
normalSequence.produceData(context, FetchConsumer(success))
}
(1)内存生产者
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 {
// 生产成功,回调上级consumer
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)
}
}
}
(2)磁盘生产者
class DiskProducer(
private val nextProducer: Producer<ChapterContent>? = null
) : Producer<ChapterContent> {
companion object {
/**
* 生产:从磁盘中取
*/
fun produce(chapter: BookChapter): ChapterContent? {...}
/**
* 消费:存入磁盘
*/
fun consume(chapter: BookChapter, chapterContent: ChapterContent) {...}
}
override fun produceData(context: DataContext, consumer: Consumer<ChapterContent>) {
if (context.readCache) {
produce(context.chapter)?.also {
// 生产成功,回调上级consumer
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)
}
}
}
(3)网络生产者
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 = !chapterContent.isLock && context.writeCache
context.fromProducer = DataContext.PRODUCER_NET
consumer.consumeData(context, chapterContent)
}
}
四、三级缓存的两个细节
网络加载数据后,读取「Response」数据流,会使用「MemoryPooledByteBufferOutputStream」保存
该类使用「MemoryChunkPool」,回收复用同样大小的内存空间,防止内存抖动
(malloc分配一块指定大小的内存,并返回指向这块内存开始地址的指针,这块内存在native heap中)
可以复用到章节内容下载上,尤其是批量下载
Fresco的读写磁盘是分开两个「Producer」的,读写磁盘使用了「BufferedDiskCache」,一个自定义的缓存封装
写入磁盘使用了临时文件写入,最后写入完成后将临时文件移入目标位置
这是写大文件的常用操作,保证写入目标区域时操作的原子性,也可以避免目标区域被脏数据污染
了解这个后也就可以明白源码中的这个注释:使用临时文件写入可以允许多个文件并行写入
随着产品设计的不断冲击,需求逐渐复杂,越来越需要一套健壮的代码,网文阅读器会继续参考借鉴,一步步调优代码。