您的位置:首页 > 移动开发 > Android开发

[置顶] 设计模式-单例模式(Singleton)在Android中的应用场景和实际使用遇到的问题

2016-12-01 18:16 686 查看


介绍

上篇博客中详细说明了各种单例的写法和问题。这篇主要介绍单例在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;
}
1
2
3
4
5
6
7
8
9
10
11
12
1
2
3
4
5
6
7
8
9
10
11
12

最后在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> S createService(Class<S> serviceClass) {
Retrofit retrofit = builder.client(httpClient.build()).build();
return retrofit.create(serviceClass);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

只所以这么写,采用静态类而不是单例,是因为把网络访问看做工具类,只需要拿到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中很多意想不到的问题


解决办法

用单例传递数据实质是用内存存储数据,然后全局方法。但是内存是很容易被虚拟机回收的。我们要解决的就是怎么样保存数据,持久化数据。 

其实也没有什么好的解决方案。
还是直接将数据通过intent传递给 Activity 。
使用官方推荐的几种方式将数据持久化到磁盘上,再取数据。
在使用数据的时候总是要对变量的值进行非空检查,这样还是取不到数据
使用EventBus传递数据时采用onSaveInstanceState(Bundle outState)方法保存数据,使用onCreate(Bundle savedInstanceState)等待恢复取值。


包装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);
}

@Override
protected void onCreate(Bundle savedInstanceState) {
//获取数据
mKey = getIntent().getStringExtra(TYPE_KEY);
mTitle = getIntent().getStringExtra(TYPE_TITLE);}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

使用代码,就一行
MyActivity.launch(this, key, title);
1
1

整个的逻辑是,在跳转的组件中实现类方法,把传递值的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插件快捷成成代码。


总结

总算写完了,一个单例模式写了两篇博客,上篇博客主要是说明各种单例的写法和分析。
本文主要介绍比较新的枚举单例
还有单例的应用场景和思考,以及在Android开发中单例的应用场景。单例模式其实在源码和很多开源框架中都有应用,写好单例分析单例的和静态类方法和适用场景能够写出好的代码。
最后总结我在使用单例时遇到的坑和提出解决方案。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: