博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
享元模式在Android中的简单应用
阅读量:3736 次
发布时间:2019-05-22

本文共 6792 字,大约阅读时间需要 22 分钟。

享元模式是常见设计模式中的一种,其目的是为了复用已经创建的对象,而避免在程序中短时间内产生大量重复的对象,而这些对象又在短时间内失去引用,从而又变成可回收状态,等待虚拟机回收,以至于消耗不必要的资源。

享元模式并没有什么固定的范本式的实现代码,其核心思想就是缓存对象。我们在这里先举一个享元模式在Android Framework层中应用的例子。

Android消息机制想必大家都不陌生,无论理解的深还是浅,在我们初学Android的时候,至少都使用过Handler在子线程中更新UI;Handler,Looper,MessageQueues,Message这四者是密不可分的一整套系统,而其中,这个Message就是使用了享元模式的一个典型例子。我们在创建一个Message对象的时候,最好是应该使用如下代码:

Message message = Message.obtain();
当然你也可以使用Message的构造方法:

Message message = new Message();
这样写系统也并不会报错,但是这两种写法是有区别的。这里由于篇幅原因,我只简述一下,使用构造方法,也就是new关键字,其实也就是等同于在告诉虚拟机,我要申请一块内存区域,来存放我即将新构造的一个对象,如果程序中不停的new出新对象,那对象就会堆积如山,最终塞满整个内存,导致内存溢出(这里提示一下,一直被强引用持有的对象,是不会被虚拟机回收的)或者频繁触发虚拟机的gc机制。

但是obtain()方法则并不是这样,它在内部使用了一个链表来缓存Message对象,也就是说,一开始,如果用户想使用Message对象,它会进行创建,当这个Message对象被使用完成之后,它并不会失去引用,成为在内存中等待回收的“垃圾”,而是被清除数据,然后重新加入到链表头部,这样,当用户多次通过obtain()方法获取Message对象以后,用户得到的Message对象则可能是之前创建而后又被缓存到链表中的。

在Message中,享元模式的具体实现是链表,而其它使用场景中,缓存对象的容器实际上是可以随便切换的,例如你想用HashMap之类的也是可以的。

说到这里,我们简单介绍了一下享元模式的思想,说白了就是重复使用之前创建的对象呗,不错,接下来我将介绍几个实例,都非常简单,甚至不需要我们使用容器类。

在前几年,Android网络请求库中比较流行Volley,当然,Volley还有一个附带功能就是加载图片,通常情况下,你需要一个ImageLoader对象,在Volley的图片加载中,除非你使用最基本的ImageRequest,否则,无论是使用ImageLoader方式还是NetworkImageView方式,都需要一个ImageLoader对象,如果是使用ImageLoader方式,则是调用ImageLoader的get方法,如果使用NetworkImageView方式,则是把ImageLoader作为参数,从setUrl方法中传入,但实际上,传入后的ImageLoader还是调用了get方法,简单介绍完了之后,我们来分析一个事情,ImagwLoader在这里实际上是一个加载器的角色,也就是说,当你使用它的时候,实际上是它ImageLoader做了“图片加载这件事”,在这里,ImageLoader是一个工作者的角色,因此我们可以简单设想,我们每次加载图片的时候是否可以都使用同一个ImageLoader对象,让这个工作者做多份工作,而不是每次一有图片加载任务的时候就创建一个ImageLoader,然后当这个任务结束它就“失业”?我怀着这样的猜想,把ImageLoader放在了Application中,让整个app全局内只存在这一个ImageLoader对象,每次需要加载图片的时候,通过我编写的get方法,去Application中去取,而不是每次都创建一个新的ImagrLoader,事实证明这是可行的。因此,这唯一的一个ImageLoader,就被多次复用,从而变成了一个被“共享”的对象。

Volley毕竟是前几年的主角,最近两年刚刚开始学习Android开发的小伙伴也许对它并不熟悉,对上面我举的例子也没啥概念。那我现在举一个如今非常流行的一个网络库——Retrofit的例子。

Retrofit的使用我也不多说了,总之就是首先需要创建一个Retrofit对象,然后给每一种请求都创建一个写有相关参数的接口,然后通过Retrofit对象来创建这些接口的实例,最终通过这些接口的实例来发出网络请求。Retrofit在这一列事情中的角色是接口实例的创建者,所以它也是一个“工作者”,因此,把它放到Applicaction中就无可厚非了。那接口实例呢?

我们按照之前的流程,先给一个Retrofit发网络请求的例子。

首先我们定一个接口:

public interface IGetDiaryService {        @GET("diary.php")        Call
get(@Query("diary_ID") String diaryId); }
然后写一个用来发起网络请求的方法:

private void getDiary(boolean ifSet) {        IGetDiaryService service = MyApplication.getRetrofit().create(GetDiaryService.IGetDiaryService.class);        Call
call = service.get(entries.getId()); call.enqueue(new Callback
() { @Override public void onResponse(@NonNull Call
call, @NonNull Response
response) { initView(); tvTitle.setText(entries.getTitle()); String content = response.body().getContent(); tvContent.setText(content); tvMonth.setText(DateConversion.MonthConversion(entries.getMonth())); tvDate.setText(entries.getDate()); tvWeek.setText(DateConversion.weekConversion(entries.getWeek())); String location = response.body().getLocation(); tvLocation.setText(location.equals("无") ? "没有位置信息" : location); int imageCount = response.body().getImageCount(); Glide.with(fragment).load(SpinnerImage.getWeatherList() .get(entries.getWeather() - 1)).into(imWeather); Glide.with(fragment).load(SpinnerImage.getMoodList() .get(entries.getMood() - 1)).into(imMood); if (ifSet) { entries.setContent(content); entries.setLocation(location); entries.setImageCount(imageCount); MainActivity.setTodayEntries(entries); } setGridView(imageCount); } @Override public void onFailure(@NonNull Call
call, @NonNull Throwable throwable) { Log.d("日记加载", throwable.getMessage()); Snackbar.make(recyclerView, "不好意思,加载发生了错误,请稍后再试", Snackbar.LENGTH_LONG).show(); } }); }
这是我写的一个小demo中的一段代码,onResponse中的具体逻辑可以忽略。我们可以看到,在这个方法的第一行,就是使用Retrofit创建一个IGetDiaryService接口的实例,这个方法的作用在我的小app中是为了从服务器加载日记,假如app的用户多次使用从网络加载日记这个功能,也就是说他可能会多次调用getDiary这个方法,那么这个时候,每调用一次,就会创建出一个IGetDiaryService实例,而之前创建的实例由于上一次执行getDiary方法完毕,强引用实效,从而进入到了可回收状态。如果用户短时间内多次调用此方法,就会在内存中遗留下一大堆IGetDiaryService的待回收垃圾。这肯定是相对来说不好的。

那我们现在该怎么办,使用一个全局变量来缓存IGetDiaryService吗?这时候我不得不考虑另一个问题,在具体的业务中,app用户如果使用加载日记这个功能,则有可能在短时间内多次使用,但是一旦用户不想再看日记了,转而去做其它操作了,那可能这次使用本app就不会再使用加载日记这个功能了,这时候,如果还一直使用全局变量这种强引用来缓存对象则实际上造成了某种意义上的内存泄漏(一个再也不会被使用的对象,一直占用内存空间),这时候我们该怎么办呢?答案当然是使用软引用或者弱引用,这里,我们需要把IGetDiaryService接口包装一下:

public class GetDiaryService {    private static SoftReference
serviceSoftReference; public static IGetDiaryService getService() { if (serviceSoftReference == null || serviceSoftReference.get() == null) { IGetDiaryService service = MyApplication.getRetrofit().create(IGetDiaryService.class); serviceSoftReference = new SoftReference<>(service); } return serviceSoftReference.get(); } public interface IGetDiaryService { @GET("diary.php") Call
get(@Query("diary_ID") String diaryId); }}
看吧,我们使用一个软引用保存了IGetDiaryService对象,这样就较好的避免了上面所说的几个问题,每个IGetDiaryService对象既可以被复用,又能避免在没用的时候占据内存。

我通过以上两个比较详细的例子说明了在Android中哪些情况下,应该复用对象,我相信在大量实战中,我们会共同找到更多可以被复用的对象,这样,我们把它们都做成共享的,那么就可以大幅提升性能,特别是那些占据内存空间大,复用次数又多的对象,意义明显。

还有最后两件事情,第一件,我们来总结一下,具体什么样的对象是可能可以复用的。首先,就是我上面说的那种,充当“工作者”的对象,它们往往不会通过调用某些方法,来接收大量改变其自身字段的参数,它们的作用一般来说是任务的主要承担者,它们是“做”任务的,而不是被别人“做”,这类对象通常可以复用。与这类对象相反就有另一种对象,例如,主要是Data类的对象,Data类就是那些在你的app中扮演业务角色的类,例如用户,文章,评论,等等,它们通常都有大量的字段,并包含了每个字段的get/set方法,这样的对象通常都非常有“个性”,比如在你的app中有两个文章类的对象,但它们显然是代表两篇不同的文章,它们有不同的标题,内容,作者等字段,这两个对象是绝对不容混淆的。像这样的类的对象,通常是很难找到复用场景的(但这不是绝对的,要看具体场景,例如最上面讲的Message就是这种情况的反例,每个Message对象是在用完以后被清除所有字段从而实现复用的)。还有一类对象,它们常常充当“配置项”的角色,例如,在使用RecyclerView时,我们要传入布局管理器,还要传入这个RecyclerView的动画,而布局管理器,动画,等等这些对象就是“配置项”,它们被作为参数传入以后,通常只会被别的对象只做“读”操作,而不会被进行“写”操作,说白了就是不会改变它们内部的行为,这样的对象,在很多情况下也是可以复用的。

最后一件事,在哪些情况下,我这里举出的复用对象的方法是不适用的。

首先,如果你的app结构非常复杂,而你要做对象复用,会大量增加新的类和方法,从而导致你的代码可读性和结构性大幅降低的情况下,特别是这个对象如果本身占用内存又不大时,就不要使用;无论在什么时候,把代码搞乱都是一个值得慎重考虑的事情,我记得《Effective Java》中说过,最好的优化就是保持代码的整洁,然后不要优化,当代码结构清晰,容易阅读时,性能会随之而来,当你的程序中有大量刻意优化的代码时,也许这些被优化过的地方的局部性能会有所提升,但是由于破坏了整体架构,整个程序在执行起来的效率也许会明显下降,这种以全部局部的做法是相当不值得推崇的。

第二种情况,注意高并发时的资源竞争问题。试想一下,如果被你复用的对象会在高并发场景中被使用,那多个线程就会同时竞争同一个资源从而造成无法想象的错误后果。你也许会说,那上锁不就行了吗?确实,上锁能避免造成无法想象的错误后果,但是同时,并发也就失效了,因为同一时刻,这个资源只能被同一个线程使用,其它多个线程都只能等待这个线程使用完毕,当第一个线程使用完毕以后,第二个线程又占据资源,而剩下的线程又处于等待状态,这样一轮又一轮的等待下去,会耽误大量时间,而并发在这里由于资源有限,也变得毫无意义。如果你仍然坚持在这种情况下复用对象,我觉得有一个好方法就是使用ThreadLocal,在每个线程内都储存一个独立的该对象给每个线程使用,这样,相对来说是一种两方权衡的结果,因为既不会创建出远多于线程数量的对象,又规避了资源竞争问题,合理的分配了资源。

享元模式也就介绍这么多了,它只是一种复用对象的思想,至于到底如何复用,就要看你的具体业务逻辑的情况而定啦。

转载地址:http://zopin.baihongyu.com/

你可能感兴趣的文章
线程中断处理
查看>>
消息队列积压问题处理
查看>>
并行流使用注意事项
查看>>
泛型擦除机制及相关问题
查看>>
Jackson日期反序列化时区问题
查看>>
《设计模式》
查看>>
单例设计模式
查看>>
面试题集锦(一)
查看>>
Calendar类方法——编写万年历的两种方式
查看>>
File类的使用——遍历所有文件及子文件以及遍历删除
查看>>
内存流的使用——基本使用
查看>>
RandomAccessFile 类的使用——基本使用
查看>>
Properties实现类——基本使用
查看>>
结构型模式——装饰者设计模式
查看>>
线程的同步——Synchronized和ReentrantLock
查看>>
网络编程基础
查看>>
python实现快速排序
查看>>
python实现归并排序
查看>>
二叉树的镜像实现(python版)
查看>>
ptqt5控件了解(三)
查看>>