设计模式-单例模式(Singleton)在Android中的应用场景和实际使用遇到的问题
介绍
在上篇博客中详细说明了各种单例的写法和问题。这篇主要介绍单例在Android开发中的各种应用场景以及和静态类方法的对比考虑,举实际例子说明。
单例的思考
写了这么多单例,都快忘记我们到底为什么需要单例,复习单例的本质
单例的本质:控制实例的数量
全局有且只有一个对象,并能够全局访问得到。
控制实例数量
有时候会思考如果我们需要控制实例的数量不是只有一个,而是2、3、4或者任意多个呢?我们怎样控制实例的数量,其实实现思路也简单,就是通过Map缓存实例,控制缓存的数量,当有调用就返回某个实例,这其中就涉及到调度问题。考虑在实际Android开发中有这样的情况吗?还真有,如果看过我的上篇分析单例的博客提到郭神和洪洋大神都有LruCache实现图片缓存,不就是控制实例数量的应用场景吗。LruCache内部用LinkedHashMap持有对象。用LruCache缓存图片到内存,图片数量就是我们需要控制的实例数量,一般是根据内存的大小开空间存图片,根据图片地址url取内存中的图片没有访问网络获取,内部采用最近最少使用调度算法控制图片的存储。
具体实现看比较复杂,详情去看两位大神的CDNS博客吧。
单例的应用场景
Android开发中单例模式应用
单例在Android开发中的实际使用场景,图片加载框架就是一个很好的例子。我在刚接触Android的时候使用的Android Universal Image Loader
就采用了单例,这是因为它需要缓存图片,对缓存的图片集合做各种操作,需要关注单例中的对象状态,而且明显是需要访问资源的。这就很契合单例的特性。同样在热门的EventBus中也采用了单例,因为它内部缓存了各个组件发送过来的event对象,并负责分发出去,各个组件需要向同一个EventBus对象注册自己,才能接收到event事件,肯定是需要全局唯一的对象,所以采用了单例。
EventBus的单例采用的是双重检查加锁单例
static volatile EventBus defaultInstance;public static EventBus getDefault() {if (defaultInstance == null) {synchronized (EventBus.class) {if (defaultInstance == null) {defaultInstance = new EventBus();}}}return defaultInstance;}
最后在Android源码中发现,一个非常重要的类LayoutInflater本身也采用的是单例模式。
单例的替代
回到开发的场景中,思考我们为什么需要单例。如果是需要提供一个全局的访问点用getInstance()
做些操作。除了单例我们还有其他的选择吗?
回去翻看Android源码,有这样一个类。java.lang.Math类它提供对数字的操作和方法计算,它的实现就是全部方法用static修饰符
包装提供类级访问。因为当我们调用Math类时只要它的某个类方法做数据操作并不关心对象状态。
单例不需要维护任何状态,仅仅提供全局访问的方法,这种情况考虑使用静态类,静态方法比单例更快,因为静态的绑定是在编译期就进行。
如果你需要将一些工具方法集中在一起时,你可以选择使用静态方法,但是别的东西,要求单例访问资源并关注对象状态时,应该使用单例模式。
Retrofit框架静态类构造工具类
在我的一个项目中使用到Retrofit做网络访问,这就需要一个具体的Retrofit对象操作网络。而且最好提供方法得到这个全局唯一的Retrofit对象。一开始我也在纠结是单例还是静态类。因为国内网站上对Retrofit的分析使用不是很多,而且网络上对这单例和静态类的分析争辩实在太多而且混乱。
最后直到看到这篇博客,感觉还是老外靠谱,最后我的项目采用下面的代码实例化Retrofit对象。具体代码是这样的。目前使用没有问题,大家当做使用Retrofit时候的实例化参考吧。(代码依据最新的Retrofit-2.0版本)
public class ServiceGenerator {public static final String API_BASE_URL = "http://your.api-base.url";private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder();private static Retrofit.Builder builder =new Retrofit.Builder().baseUrl(API_BASE_URL).addConverterFactory(GsonConverterFactory.create());public static S createService(Class serviceClass) {Retrofit retrofit = builder.client(httpClient.build()).build();return retrofit.create(serviceClass);}
}
只所以这么写,采用静态类而不是单例,是因为把网络访问看做工具类,只需要拿到Retrofit实例对象做网络操作,ServiceGenerator工具类内部不维护内部变量也不关心内部变量的状态变化。
单例开发实际问题
踩坑是每个开发者必须经历的过程,下面说明我在采用单例之后遇到的坑。相信每个初级Android开发者都遇到这样的问题。两个Activity组件之间传递数据,Intent和Bundle只能传递简单的基本类型数据和String对象
(当然也可以传递对象这就需要Parcelable和Serializable接口)。
当需要传递的只是几个值问题不大,但是如果需要传递的数据比较多就感觉代码不简洁而且key值多容易接收出错,传递对象需要对象继承Parcelable接口写大量的重复的模板代码。有没有优雅一点解决办法呢?
用单例对象传递对象的坑
Application传递对象的坑
相信有些人跟当时的我一样看过这样的博客”优雅的用Application传递对象”。当时的我看见这样博客,真实感觉遇到救星一样,感觉一下就解决了组件间传递对象的问题。
长者语:too young too simple sometimes naive
下面来说说如果你真的用Application传递对象会怎么样。原文博客是这样认为的Application由系统提供是全局唯一的对象,并且任何组件都可以访问到。哪就在自定义继承Application的子类里,保存内部变量,由发送的Activity取出内部变量并设值,startActivity之后在接收的Activity中也访问Application对象取出内部变量得到需要传递的对象。就没有复杂的Intent传值了。
但是如果你真的这么做:程序肯定会崩或者是取不到数据。
实际运行情况是这样的:
1. 如果你在接收数据的Activity中,按下Home键返回桌面,长时间的没有返回你的App。
2. 系统有可能会在系统内存不足的时候杀掉进程。
3. 当你再从最近程序运行列表进入你的App,系统会默认恢复刚刚离开的状态,直接进入接收数据的Activity中。
4. 然后调用各个生命周期方法回调,其中只要运行到从Application取数据行,程序就会弹出空指针NullPointerException异常导致崩溃。
5. 相信我一定是这样的,如果没有崩溃也只是因为你在内部变量中有默认初始化方法。这样肯定也是取不到想要的数据。
因为整个流程需要很长时间,我们可以使用adb命令杀掉进程adb shell kill
,模拟长时间没有回到应用而由系统杀死进程的操作。如果觉得麻烦还可以打开Device Monitor-选中你的应用-使用红色按钮 Stop Process杀死进程。
程序崩溃的这主要原因就是:
系统会恢复之前离开的状态,直接进入某个Activity组件而不是再依次打开Activity,这样你的发送数据的Activity没有运行也就不会向Application中传值,自然也取不到值。
所以千万不要相信”优雅的用Application传递对象”这写博客,这是个坑!实际情况复杂得多,真使用起来还有很多问题。
指出这个问题原文是dont-store-data-in-the-application-object中文翻译的博客在这,大家可以点击查看会有详细说明。
EventBus的坑
当时也是在写一个项目,觉得Intent传递数据太麻烦,根据Appliaction可以传递数据的思路,其实自己也可以写个单例用来保存全局数据,各个组件取出实现组件间传递数据。然后很网络上搜索,发现EventBus同样实现了这样的思路,EventBus本身就是采用了单例模式。上篇博客的伏笔就在这。
EventBus: Android 事件发布/订阅框架,通过解耦发布者和订阅者简化 Android 事件传递
由一个组件发送事件,另一个组件向EventBus注册然后响应的方法就会得到数据。这里面也有坑啊。
当然我没有说EventBus有问题,只是使用不当会导致Crash程序崩溃。
当时项目是就是按照标准的EventBus使用流程写的代码,没有问题。还是上文的情况,按下Home键长时间没有返回应用,再次进入程序Crash。
原因还是一样的:
系统恢复离开的现场,直接运行接收数据的Activity,而没有运行到发送数据的Activity组件,取不到数据,因为根本就没有数据发送。
顺带提一句,
用Kill App这个方法能够检查出App中很多意想不到的问题
解决办法
用单例传递数据实质是用内存存储数据,然后全局方法。但是内存是很容易被虚拟机回收的。我们要解决的就是怎么样保存数据,持久化数据。
其实也没有什么好的解决方案。
包装Activity跳转方法
针对第一项,我提供一个简单的包装跳转方法,简化Inten传递数据的代码逻辑
public class MyActivity extends AppCompatActivity{//Intent的key值protected static final String TYPE_KEY = "TYPE_KEY";protected static final String TYPE_TITLE = "TYPE_TITLE";//接收的数据public String mKey;public String mTitle;//包装的跳转方法
public static void launch(Activity activity, String key, String title) {Intent intent = new Intent(activity, BoardDetailActivity.class);intent.putExtra(TYPE_TITLE, title);intent.putExtra(TYPE_KEY, key);activity.startActivity(intent);}@Overrideprotected void onCreate(Bundle savedInstanceState) {//获取数据mKey = getIntent().getStringExtra(TYPE_KEY);mTitle = getIntent().getStringExtra(TYPE_TITLE);}}
使用代码,就一行
MyActivity.launch(this, key, title);
整个的逻辑是,在跳转的组件中实现类方法,把传递值的key值以成员类变量的形式写定在Activity中,需要传递的数据放入Intent中,简化调用方的使用代码。
onSaveInstanceState保存数据
onSaveInstanceState()方法的调用时机是:
只要某个Activity是做入栈并且非栈顶时(启动跳转其他Activity或者点击Home按钮),此Activity是需要调用onSaveInstanceState的,
如果Activity是做出栈的动作(点击back或者执行finish),是不会调用onSaveInstanceState的。
这正是上文我们程序Crash的场景,产生问题的关键操作点。
所有我们需要做的就是在onSaveInstanceState回调方法中保存数据,等待数据恢复。
代码没什么好贴的就是outState.putParcelable(KEY, mData);
,然后在OnCreate中取savedInstanceState中的数据。
提示被put的数据需要实现Parcelable接口,如果不想写大量的模板代码可以使用Android Parcelable Code Generator插件快捷成成代码。
总结
标签:
相关文章
-
无相关信息