面试官:让你设计一套图片加载框架,你会怎么设计?

简介: 很多同学在面试中都会被问到图片加载这块的知识。
🔥 Hi,我是小余。

本文已收录到 GitHub · Androider-Planet 中。这里有 Android 进阶成长知识体系,关注公众号 [小余的自习室] ,在成功的路上不迷路!

前言:

很多同学在面试中都会被问到图片加载这块的知识。

下面是一段模拟面试

面试官:图片加载有了解么?

我..

面试官:图片是怎么缓存的?

我..

面试官:Glide了解过吧,说说你对Glide的看法?

我...

面试官:让你自己设计一套图片加载框架, 你要怎么设计?

面试官会循序渐进的方面问你,最终目标可能就是让你自己设计一个图片加载框架
你会怎么设计?先不要看下文,自己好好想想,设计思路是什么?

图1.webp
...

好了,没想好也没关系,看完这篇文章你就会懂了。

我们来聊下我们平时使用的网络请求都需要有哪些步骤

  • 1.组装我们的请求实体类,内部封装了请求需要的urlheader,以及一些参数信息
  • 2.通过请求信息,去内存缓存中获取,没有再去本地磁盘缓存中查找是否有缓存数据,如果有,记得磁盘中缓存的数据是元数据,需要进行解码后,才可以使用,解码后的数据通过回调或者Handler传递给业务层
  • 3.没有缓存数据,则需要去服务器上拉取数据,这个过程需要使用异步加载的方式,线程池也许是个好选择
  • 4.获取数据后,先将元数据缓存到磁盘,再将图片数据解码,解码后,缓存到内存缓存中,最后回调解码后的响应数据给业务层。

以上就是一个完整的图片加载请求的流程:

用一个图来表示:

网络请求框架.awebp

我们从流程中考量下我们需要做的工作:

根据第一步说明:

组装我们的请求实体类,内部封装了请求需要的url,header,以及一些参数信息

这里思路

1.设计一个Request的接口,接口中可以调用开始请求和暂停请求,以及获取我们的请求状态等方法,

public interface Request {
    /**
    开始请求
    */
    void begin();
    /**
    清除当前请求数据
    */
    void clear();
    /**
    暂停请求
    */
    void pause();
     /**查看请求正在请求中 */
    boolean isRunning();

    /** 查看请求是否成功获取数据,并完成 */
    boolean isComplete();

    /** 查看请求是否被清除 */
    boolean isCleared();

}

2.实现这个Request接口,假设为SingleRequest,在这个类中可能需要有
请求状态,传入的请求控件的长宽属性,请求的url信息,请求参数信息:

public final class SingleRequest<R> implements Request {
    //这个url为什么是一个object呢,因为不一定是传入的是String,有可能是封装了String url的其他类
    private final Object url;
    /**控件的长宽*/
    private int width;
    private int height;
    
    /**请求的参数信息,这个是外部需要作出可配置的*/
    private final BaseRequestOptions<?> requestOptions;
    
    //这里是一个请求的状态信息,加载过程中可以根据状态不同,做不同的操作
    private enum Status {
        /** Created but not yet running. */
        PENDING,
        /** In the process of fetching media. */
        RUNNING,
        /** Waiting for a callback given to the Target to be called to determine target dimensions. */
        WAITING_FOR_SIZE,
        /** Finished loading media successfully. */
        COMPLETE,
        /** Failed to load media, may be restarted. */
        FAILED,
        /** Cleared by the user with a placeholder set, may be restarted. */
        CLEARED,
    }
    ...
    //当然这里面需要实现Request接口中的方法
    ...
        ...
}

根据步骤2:

通过请求信息,去本地缓存中查找是否有缓存数据,如果有,直接使用缓存中的数据,通过回调或者 Handler传递给业务层

这里我们可以设计一个三级缓存:分别为:

  • 磁盘缓存:DiskLruCache,网络上有很多开源框架,可以直接拿过来用,或者根据自己需求设计一套,建议使用工厂模式创建,这样可以更好的对外扩展
  • 内存缓存:MemoryCache:这个缓存中持有缓存数据的强引用,防止在gc时被回收,这个也可以做成一个接口,接口封装方法如下:

一些基本操作:putremoveclear清空缓存,监听设备内存状况,及时回收缓存,防止OOM

public interface MemoryCache {
    //首先需要有put方法
    Resource<?> put(@NonNull Key key, @Nullable Resource<?> resource);
    //需要有remove方法
    Resource<?> remove(@NonNull Key key);
    //清空缓存
    void clearMemory();
    //监听内存状态
    void trimMemory(int level);
    //获取当前缓存大小信息
    long getCurrentSize();
    ...
    其他方法
}

使用一个LruResourceCache 去实现MemoryCache接口中的方法。
我们了解到Android系统有为我们提供一个LruCache的内存缓存类,让LruResourceCache直接去继承这个LruCache即实现了我们的内存缓存。

继承关系如下:

public class LruResourceCache extends LruCache<Key, Value> implements MemoryCache

然后我们的所有的缓存请求,都可以直接提交给父类LruCache去做,只实现MemoryCache接口中的方法即可,有点代理模式那味了..

那可能有同学要说了,你这个内存缓存,如果装的太多,不是会内存溢出么甚至出现OOM么?
好,那就需要引入我们的第3级缓存了:

  • 弱引用缓存:我们创建一个ActiveResourcesCache:这个类也是一个内存缓存,那又有同学要说了,你前面不是设计了一个缓存了么,为啥又来设计一个内存缓存

别急别急。。

我们来说下这个缓存和前面的MemoryCache有啥区别:
这个类中我们使用一个包含弱引用的HashMap来存储当前缓存数据

Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();

创建一个ReferenceQueue来监听当前缓存数据被回收状态

private final ReferenceQueue<EngineResource<?>> resourceReferenceQueue = new ReferenceQueue<>();

这样做的目的是什么呢?

  • 1.在内存紧张需要回收资源的时候,首先回收的就是弱引用中的缓存,这样可以防止应用出现OOM。
  • 2.在某些情况下,我们可以将数据放入到二级缓存MemoryCache中,防止被回收。

缓存获取方法:
先去三级缓存ActiveResourcesCache中获取弱引用数据,如果取不到,则再去二级缓存MemoryCache中获取,最后才是磁盘缓存中获取。

上面就很好的实现了我们的三级缓存机制

好了步骤2我们就讲到这里

继续步骤3:

没有缓存数据,则需要去服务器上拉取数据,这个过程需要使用异步加载的方式,线程池也许是个好选择

这个步骤我们可以提取哪些信息呢:

1.异步加载:因为图片加载都是短时间并发加载,所以需要使用线程池来解决,建议把线程池的核心线程数设置为0,非核心线程数设置一个较大的值,尽量满足多并发需求

public final class AndroidExecutor implements ExecutorService {
    //这里面可以根据具体需求创建不同类型的线程池
    //
    public ThreadPoolExecutor getThreadPollExcutor{
        return new ThreadPoolExecutor(
              corePoolSize,//0
              maximumPoolSize,//10
              /*keepAliveTime=*/ threadTimeoutMillis,
              TimeUnit.MILLISECONDS,
              new PriorityBlockingQueue<Runnable>(),
              new DefaultThreadFactory(
                  threadFactory, name, uncaughtThrowableStrategy, preventNetworkOperations));
    }
}
  • 图片加载过程因为有很多的不同状态:如初始化init状态,数据加载running,数据加载Success,加载失败Fail,数据缓存操作,原数据解码等操作.

可以使用一个Job类来管理我们的网络加载过程:

class Job<R> implements Runnable{
    public void run{
        执行这里面的run逻辑,对不同的Job状态做不同的处理
    
    }
}

每个Job代表一个状态,需要处理的逻辑,可能同一个Job下需要连续处理多个状态。

来看步骤4:

获取数据后,将图片数据解码,解码后,缓存到本地,然后回调解码后的响应数据给业务层。

我们抽取关键信息:图片解码图片缓存回调响应业务层

  • 图片解码:因为是图片在一些低内存的设备上,可能还需要做降采样,裁剪等处理:

解码逻辑如下:

- 1.先使用options.inJustDecodeBounds = true:获取到图片的宽和高

- 2.获取实际控件Target的宽和高,和1做对比对数据进行裁剪

- 3.然后将原数据解码成需要的尺寸
  • 图片缓存

我们做两次缓存,获取服务器请求的元数据后,缓存到磁盘,这个步骤需要异步处理。
然后对元数据解码,得到后的数据,放入内存缓存中,这样在取的时候,优先去内存缓存中获取缓存,
没有数据才会去磁盘中获取数据,磁盘中的数据是元数据,需要使用解码才能被控件使用

其实数据加载前和加载后是一个层层对应的关系,包括我们的缓存处理逻辑

  • 回调响应业务层:

这个我们封装好响应数据使用主线程回调的方式给业务层。

好了,以上就是设计一个标准的网络请框架需要流程。

除了上面的这些还需要注意哪些么?

1.我们想象下如果一个控件被 销毁了,但是我们的任务还在继续会出现什么情况呢?对了,会出现 内存溢出的情况
所以我们需要做好控件生命周期和任务的管理。
管理生命周期需要和你传入的 target关联,
如果是 Activity或者 FragmentActivity,可以监听这些组件的生命周期
如果是 Application的,需要监听 Application生命周期

还有什么要补充的么?

对了,还有图片加载动画,列表动画加载 View的复用, Bitmap复用和回收等,都是我们要考虑的方面。

还有么??

这次是真没有了。。

最后来总结下前面所讲的:

设计一个图片加载框架需要注意的地方:

  • 1.异步加载数据:线程池
  • 2.线程切换:Handler
  • 3.需要支持多种图片格式加载
  • 4.使用多级缓存:磁盘,内存,弱引用对象,
  • 5.防止OOM:弱引用,图片字节数组存放位置如native
  • 6.生命周期管理
  • 7.资源解码降采样
  • 8.Bitmap回收和复用

总结

细心的朋友可能发现,我上面讲解的内容都是Glide里面实现了的,这些大型开源框架已经帮我们处理好了,
我们只需要复用就可以,没必要重复造轮子些大型开源框架已经帮我们处理好了,我们只需要复用就可以,没必要重复造轮子,
但是框架的原理你一定要清楚。

相关文章:Android体系课-开源框架-这是一份详细的Glide源码分析文章

如果您有不同看法,欢迎留言

相关文章
|
6天前
|
中间件 数据库连接 API
Python面试:FastAPI框架原理与实战
【4月更文挑战第18天】FastAPI是受欢迎的高性能Python Web框架,以其简洁的API设计、强大的类型提示和优秀的文档生成能力著称。本文将探讨FastAPI面试中的常见问题,包括路由、响应对象、Pydantic模型、数据库操作、中间件和错误处理。同时,还会指出一些易错点,如类型提示不准确、依赖注入误解,并提供实战代码示例。通过理解和实践FastAPI,可以在面试中展示出色的Web开发技能。
31 1
|
7月前
|
XML Java 应用服务中间件
面试官问我咋实现Spring框架IOC和DI好吧打趴下,深度解析手动实现Spring框架的IOC与DI功能
面试官问我咋实现Spring框架IOC和DI好吧打趴下,深度解析手动实现Spring框架的IOC与DI功能
49 0
|
6天前
|
API 数据库 数据安全/隐私保护
Flask框架在Python面试中的应用与实战
【4月更文挑战第18天】Django REST framework (DRF) 是用于构建Web API的强力工具,尤其适合Django应用。本文深入讨论DRF面试常见问题,包括视图、序列化、路由、权限控制、分页过滤排序及错误处理。同时,强调了易错点如序列化器验证、权限认证配置、API版本管理、性能优化和响应格式统一,并提供实战代码示例。了解这些知识点有助于在Python面试中展现优秀的Web服务开发能力。
30 1
|
6天前
|
缓存 算法 自动驾驶
百度Cyber框架面试总结
百度Cyber框架面试总结
11 0
|
6天前
|
SQL 中间件 API
Flask框架在Python面试中的应用与实战
【4月更文挑战第18天】**Flask是Python的轻量级Web框架,以其简洁API和强大扩展性受欢迎。本文深入探讨了面试中关于Flask的常见问题,包括路由、Jinja2模板、数据库操作、中间件和错误处理。同时,提到了易错点,如路由冲突、模板安全、SQL注入,以及请求上下文管理。通过实例代码展示了如何创建和管理数据库、使用表单以及处理请求。掌握这些知识将有助于在面试中展现Flask技能。**
19 1
Flask框架在Python面试中的应用与实战
|
6天前
|
机器学习/深度学习 分布式计算 BI
Flink实时流处理框架原理与应用:面试经验与必备知识点解析
【4月更文挑战第9天】本文详尽探讨了Flink实时流处理框架的原理,包括运行时架构、数据流模型、状态管理和容错机制、资源调度与优化以及与外部系统的集成。此外,还介绍了Flink在实时数据管道、分析、数仓与BI、机器学习等领域的应用实践。同时,文章提供了面试经验与常见问题解析,如Flink与其他系统的对比、实际项目挑战及解决方案,并展望了Flink的未来发展趋势。附带Java DataStream API代码样例,为学习和面试准备提供了实用素材。
93 0
|
7月前
|
Dubbo Java 应用服务中间件
Dubbo第二讲:深入理解dubbo分布式服务框架/负载/容错/调优/高可用/dubbo网关/面试/技术选型
Dubbo第二讲:深入理解dubbo分布式服务框架/负载/容错/调优/高可用/dubbo网关/面试/技术选型
162 0
|
7月前
|
Java Spring 容器
面试官问我咋实现Spring框架IOC和DI好吧打趴下,深度解析手动实现Spring框架的IOC与DI功能2
面试官问我咋实现Spring框架IOC和DI好吧打趴下,深度解析手动实现Spring框架的IOC与DI功能2
26 0
|
6天前
|
XML Java 数据格式
面试题:在spring框架下,创建容器对象的方式有哪些?你做项目的时候,会考虑哪种?
面试题:在spring框架下,创建容器对象的方式有哪些?你做项目的时候,会考虑哪种?
22 0
|
6天前
|
Java Spring 容器
面试题:在spring框架下面,Bean的属性lazy-init有什么作用,默认值是多少
面试题:在spring框架下面,Bean的属性lazy-init有什么作用,默认值是多少
17 0

相关实验场景

更多