孟远

每天进步一点点


  • 首页

  • 关于

  • 标签

  • 归档

程序运行更高效-原型模式

发表于 2017-07-15

模式介绍

原型模式是一种创建式模式。
用户可以从一个样板对象中复制出一个内部属性一致的对象,这个过程也就是我们常说的“克隆”。
被复制出的对象就是我们所说的“原型”,这个“原型”是可以进行定制的。
原型模式多用于创建复杂、构造耗时的对象,因为在这种情况下,利用原型模式复制一个已经存在的对象可以使程序运行更高效。

应用场景

  • 一个对象初始化时需要消耗非常多的资源;
  • 一个对象需要提供给多个对象访问,并且多个对象调用时需要进行定制。

需要注意的是,复制操作并不一定比new快,只有当new对象较为耗时或成本较高时,通过复制才能获得效率上的提升。
因此,在使用原型模式时需要对对象的构建成本进行一些效率测试。

简单示例

如何实现复制操作?
非常简单,Java提供了一个叫做Cloneable的接口,我们只需实现该接口,重写它的clone()方法即可。
现在我们来举个简单实例来演示原型模式:
假设我们现在有一篇文档对象:

1
2
3
4
5
6
7
8
9
10
public class WordDocument{
//文本
private String text;
//作者
private String author;
//图片集合
private ArrayList<String> imageList;

//....省略get、set
}

里面包含了作者、文本以及图片合集,现在我们要让这个文档实现复制功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class WordDocument implements Cloneable {
//文本
private String text;
//作者
private String author;
//图片集合
private ArrayList<String> imageList;

@Override
protected WordDocument clone() {
WordDocument document = null;
try {
document = (WordDocument) super.clone();
document.text = this.text;
document.author = this.author;
document.imageList = this.imageList;
return document;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}

//....省略get、set
}

非常简单,我们让WordDocument实现了Cloneable接口并重写了clone()方法,在clone()中进行对象的克隆操作。
那么接下来我们来实际演示下克隆效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//创建对象
WordDocument document = new WordDocument();
document.setAuthor("孟远");
document.setText("这是一篇极好的文章。");
document.addImage("图0");
document.addImage("图1");
//打印创建的对象
tv_clone_0.append("创建的对象:\n" + document.toString());
//克隆对象
WordDocument cloneBean = document.clone();
//打印克隆的对象
tv_clone_0.append("克隆的对象:\n" + cloneBean.toString());
//修改克隆的对象
cloneBean.setAuthor("黑色小老虎");
cloneBean.addImage("新加图片2");
//再次打印2个对象
tv_clone_0.append("修改克隆对象:\n" + cloneBean.toString());
tv_clone_0.append("最开始的对象:\n" + document.toString());

这段代码我们创建了一篇文章,之后拷贝了这篇文章并修改了拷贝文章。
在这期间,一共进行了4次对象的toString:

  1. 创建对象完成时打印了创建对象;
  2. 拷贝完成时打印了拷贝对象;
  3. 修改拷贝对象完成时打印了拷贝对象;
  4. 最后再次打印最初创建的对象。

这里请注意,我们修改拷贝对象时,修改了作者姓名并且添加了一张图片。
最后打印的结果如下:

细心的同学会发现,我们修改了拷贝对象的作者昵称,原对象没有受到影响。但是我们增加一张图片到拷贝对象的集合中时,原对象也发生了变化。
这就牵扯到了浅拷贝和深拷贝。

深拷贝

上述简单实例,是使用了浅拷贝来实现的。所谓浅拷贝就是直接引用原对象中的嵌套对象,不会去进行创建。
大家应该都知道对象引用的问题:

1
2
Bean a = new Bean("孟远","23","男");
Bean b = a;

此时b对象引用了a对象,也就是说其实a和b两个对象在堆内存中指向的是同一个地址,当修改b时,a也必定跟着发生变化。
同理我们再回头重新看下WordDocument的clone()代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected WordDocument clone() {
WordDocument document = null;
try {
document = (WordDocument) super.clone();
document.text = this.text;
document.author = this.author;
//问题所在,直接引用当前对象的List
document.imageList = this.imageList;
return document;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}

可以发现我们直接引用了当前对象的List,导致在内存中拷贝对象和最初对象的List指向了同一个地址。
上面代码就是我们口中的浅拷贝。
那么如何解决这个问题?
很简单,使用深拷贝,即内部对象也使用clone():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected WordDocument clone() {
WordDocument document = null;
try {
document = (WordDocument) super.clone();
document.text = this.text;
document.author = this.author;
//关建行
document.imageList = (ArrayList<String>) imageList.clone();
return document;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}

我们点进ArrayList的源码可以发现,ArrayList已经实现了Cloneable接口,所以我们直接调用ArrayList的clone()即可。
修改完这一行代码之后,再次执行上面演示代码:

完美,可以发现在修改完克隆对象之后,最开始的对象已经不会受到影响。

总结

上述演示代码已经上传至GitHub。
原型模式是非常简单的一个模式,它的核心问题就是对原始对象进行拷贝,在这个模式的使用过程中需要注意一点就是:深、浅拷贝的问题。
在实际开发过程中,为了减少错误,建议各位读者在使用原型模式时尽量使用深拷贝,避免操作副本时影响到原始对象。

感谢

《Android源码设计模式解析与实战》 何红辉、关爱民 著

建造者模式的全局Dialog

发表于 2017-07-11

这篇小文将讲述我是如何根据建造者设计模式来实现一个全局Dialog。
如果各位看官还不太了解建造者设计模式,建议可以看一下我的上篇文章。

背景

在每个项目当中,都会封装一些全局的样式,比如全局Loading、全局Dialog等。
封装这些功能是因为这些控件的使用频率极高。
在我刚接手项目A的时候,项目A也不例外,拥有着全局的Dialog。
刚开始时我尽量写一些侵入性低、仿照率高的代码,避免影响到之前的逻辑。并且学习着如何使用项目A的框架,这个过程我相信大家都是一样的。
当我用到Dialog的时候,我看到了项目A的全局Dialog,详细代码就不说了,给大家看一下项目A全局Dialog中的所有方法:


不知各位看官会不会被这么多的构造方法给吓到。
这些构造方法都是随着项目需求的增加而增加的。
大家都知道,一个完整Dialog包括的元素至少应该有:提示图片、标题、描述文字、按钮等。
但是这些元素都不是必须的:
在C页面我弹出的Dialog可能只要做一个温馨提示:一行描述文字外加一个确认按钮。
在D页面我可能弹出的是用户退出登录的二次确认窗口:标题+文字+两个按钮。
那么到这里,各位看官就能明白了,为什么会有那么多的构造方法。
没错,我们在new KLCustimDialog()的同时,需要把所有的参数都传入进去。
这种写法的问题以及维护成本之高,我就不做过多描述了,我就简单给大家加个需求:
要求点击Dialog外部不能取消Dialog。
想想这个需求下的构造方法,要添加几个?

基本

当我刚看到建造者模式的时候,我真是又惊又喜,热血沸腾!
我第一时间想到的就是重构项目A的全局Dialog!
大家都知道,Android中的AlertDialog就是使用建造者模式来实现的。
在我模仿构思了一波之后,创建Dialog的代码是这样的:

1
2
3
4
5
6
7
8
Dialog.Builder builder = new Dialog.Builder(context);
builder.setTitle("提示");
builder.setMessage("确认退出登录吗?");
builder.setLeftText("取消");
builder.setRightText("确认");
builder.setOnclickListener(listener);
Dialog dialog = builder.creater();
dialog.show();

这样写似乎没有什么问题了。
我们把Dialog的所有元素都默认隐藏,在调用某个元素的填充方法后,我们就将其显示出来。
这样我们就摆脱了无限多的构造方法,完美!
文章到这里就应该结束了?
怎么可能!我都没变形呢!

我见别人的Builder模式都是这样写的:

1
2
3
4
5
6
7
Picasso.with(context)
.load(url)
.fit()
.config(XXX)
.placeholder(xxx)
.error(xxx)
.into(imageView);

上述代码是使用Picasso来加载url图片。
一行代码完成。
帅不帅,想不想学?
所以我就想能不能用上面作为模板,来实现我们的全局Dialog。
话不多说,说干就干。

变形

完整代码我已经上传GitHub,看代码我还是建议各位看官去我的GitHub上看,比较整洁。
最终实现的效果如下:

首先,让我们来回想一下建造者模式的组成:

  • Product:产品角色
  • Builder:抽象的建造者
  • ConcreteBuilder:具体的建造者
  • Director:指挥者

接下来,我们再把这些成员转化为我们的全局Dialog的成员:

  • Product:Dialog就是我们制作出来的产品
  • Builder:Dialog参数拼接抽象
  • ConcreteBuilder:Dialog参数拼接细节
  • Director:所有用到该Dialog的地方都是指挥者,它们决定着Dialog具体样式。

思路有了,下面就开始动手吧,首先我们来创建Dialog的参数封装,里面应该有Dialog所有组成元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private static class DialogParams {
private Context context;
//标题
private String title;
//标题字体大小
private int titleSizeSp;
//图标资源
private int imageResource;
//图标宽
private int imageWidth;
//图标高
private int imageHeight;
//消息内容
private String message1;
//消息内容文字位置
private int message1Gravity = Gravity.CENTER;
//点击外部是否可以取消
private boolean isCanCancel = true;
//左边按钮内容
private String leftButtonText;
//左边按钮颜色
private int leftBtColor;
//左边点击事件
private ConcreteBuilder.ButtonClickLister leftListener;
//右边按钮内容
private String rightButtontText;
//右边边按钮颜色
private int rightBtColor;
//右边按钮点击事件
private ConcreteBuilder.ButtonClickLister rightListener;
}

我使用了内部类去实现了整个Dialog,整个Dialog只有一个类,所以所有参数都是private并且没有提供set、get。
并且我为了方便,省略了Builder抽象类,直接构造了Builder抽象类的实现ConcreteBuilder:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public static class ConcreteBuilder {
//持有Product对象
private DialogParams p;

ConcreteBuilder(Context context) {
p = new DialogParams();
p.context = context;
}
public ConcreteBuilder title(String text) {
p.title = text;
return builder;
}
public ConcreteBuilder titleSize(int spSize) {
p.titleSizeSp = spSize;
return builder;
}
public ConcreteBuilder imageResource(int imageResource) {
p.imageResource = imageResource;
return builder;
}
public ConcreteBuilder imageWidth(int imageWidth) {
p.imageWidth = imageWidth;
return builder;
}
public ConcreteBuilder imageHeight(int imageHeight) {
p.imageHeight = imageHeight;
return builder;
}
public ConcreteBuilder message(String text) {
p.message1 = text;
return builder;
}
public ConcreteBuilder messageGravity(int gravity) {
p.message1Gravity = gravity;
return builder;
}
public ConcreteBuilder canCancel(boolean isCanCancel) {
p.isCanCancel = isCanCancel;
return builder;
}
public ConcreteBuilder leftBt(String text, ButtonClickLister lister) {
p.leftButtonText = text;
p.leftListener = lister;
return builder;
}
public ConcreteBuilder leftBtColor(int color) {
p.leftBtColor = color;
return builder;
}
public ConcreteBuilder rightBtColor(int color) {
p.rightBtColor = color;
return builder;
}
public ConcreteBuilder rightBt(String text, ButtonClickLister lister) {
p.rightButtontText = text;
p.rightListener = lister;
return builder;
}
void clear() {
p = null;
}
public DialogProduct create() {
return new DialogProduct(p);
}
//按钮点击回调
public interface ButtonClickLister {
void onClick(DialogProduct dialog);
}
}

我们会发现每个参数拼接方法都会返回ConcreteBuilder,这里是实现一行代码构建Dialog的关键。
参考Picasso的书写方式,明显可以看出它没有进行new的行为,说明with()一定是静态的,随之with()返回的对象也必为静态。
为了实现Picasso的书写方式,我们这里也将ConcreteBuilder静态,方便实现一句话创建Dialog。
接下来就是Dialog的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class DialogProduct extends Dialog {

private TextView tvTitle;
private ImageView ivIcon;
private TextView tvMessage;
private TextView tvButtonLeft;
private TextView tvButtonRight;
private ImageView viewLine;

//持有Builder
private static ConcreteBuilder builder;

//模仿Picasso的书写方式
public static ConcreteBuilder with(Context context) {
if (builder == null) {
builder = new ConcreteBuilder(context);
}
return builder;
}


private DialogProduct(DialogParams p) {
//设置没有标题的Dialog风格
super(p.context, R.style.NoTitleDialog);

View contentView = LayoutInflater.from(p.context).inflate(R.layout.dialog_build, null);
setContentView(contentView);

tvTitle = contentView.findViewById(R.id.tv_title);
ivIcon = contentView.findViewById(R.id.iv_icon);
tvMessage = contentView.findViewById(R.id.tv_message);
tvButtonLeft = contentView.findViewById(R.id.tv_button_left);
tvButtonRight = contentView.findViewById(R.id.tv_button_right);
viewLine = contentView.findViewById(R.id.view_line);

//控件默认隐藏
tvTitle.setVisibility(View.GONE);
viewLine.setVisibility(View.GONE);
ivIcon.setVisibility(View.GONE);
tvMessage.setVisibility(View.GONE);
tvButtonLeft.setVisibility(View.GONE);
tvButtonRight.setVisibility(View.GONE);
//构建Dialog
setTitlText(p.title);
setTitlTextSize(p.titleSizeSp);
setImageResource(p.imageResource);
setImageWidth(p.imageWidth);
setImageHeight(p.imageHeight);
setTvMessage(p.message1);
setTvMessageGravity(p.message1Gravity);
setCancelableFlag(p.isCanCancel);
setLeftText(p.leftButtonText, p.leftListener);
setLeftBtColor(p.leftBtColor);
setRightText(p.rightButtontText, p.rightListener);
setRightBtColor(p.rightBtColor);


}
/**
* 设置标题
*
* @param title 标题文字
*/
private void setTitlText(String title) {
if (TextUtils.isEmpty(title)) {
return;
}
tvTitle.setVisibility(View.VISIBLE);
tvTitle.setText(title);
}
//......省略剩余控件代码
}

写完之后,我们来看看这个变形的建造者模式的Dialog是如何创建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
DialogProduct.with(this)
.title("提示")
.message("您确认退出登录吗?")
.canCancel(false)
.leftBtColor(getResources().getColor(R.color.color_0090ff))
.rightBtColor(getResources().getColor(R.color.color_f96c59))
.leftBt("取消", new NormalDialog.ConcreteBuilder.ButtonClickLister() {
@Override
public void onClick(NormalDialog dialog) {
dialog.cancel();
}
})
.rightBt("确认", new NormalDialog.ConcreteBuilder.ButtonClickLister() {
@Override
public void onClick(NormalDialog dialog) {
Toast.makeText(BuilderActivity.this, "退出登录成功!", Toast.LENGTH_SHORT).show();
dialog.cancel();
}
})
.create()
.show();

总结

DialogProduct的代码我已经上传到了GitHub,各位看官可以自行食用。
这个Dialog现在也是项目A中的全局Dialog,使用起来也非常方便。
这里有几个细节可以和各位看官分享一下:
第一个就是按钮的点击事件设置,我将其与按钮文字内容的设置绑定在一起。因为我认为你设置了按钮,怎么可能会没有点击事件?
第二个就是按钮中间的分割线,是与右边按钮绑定的, 所以当只有一个按钮时,我们应该使用左边的按钮leftBt而不是右边的。
基本上就是这样啦!
希望这个Dialog可以给大家带来一些灵感。

对象构造简单化-建造者模式

发表于 2017-07-05

模式介绍

Builder模式是指一步一步来构建出一个复杂的对象。
它允许用户在不知道内部构建细节的情况下,非常精细地控制对象构建流程。
该模式是为了将构建过程非常复杂的对象进行拆分,让它与它的部件解耦,提升代码的可读性以及扩展性。

模式结构

建造者模式包含如下角色:

  • Product:产品角色
  • Builder:抽象的建造者
  • ConcreteBuilder:具体的建造者
  • Director:指挥者

模式示例

我们来举个生活中的例子来描述建造者模式的各个角色:
假设我们现在是一个手机的生产商,各大手机厂商都会来找我们制造手机,比如小米、华为、三星、魅族等。每个厂商的手机配置都不一致,包括CPU型号、内存大小、像素大小等等。
现在我们将这个例子中的对象转化成建造者模式的各个角色:

  • Product:我们生产的手机就是商品,Product中应该包含手机的各个部件。
  • Builder:抽象Builder类,细节全在ConcreteBuilder中。
  • ConcreteBuilder:继承了Builder类,这里有手机的组装细节。
  • Director:这里的指挥者就是各大手机厂商,我要让你生产一台小米手机并给了你一系列参数,你就要按照参数,生产我想要的手机,其他手机也是同样的同理。

纸上谈兵终觉浅,我们将上述实例转化成代码看一看:
首先是Product,它应该包含手机的零件元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class PhoneProduct {

private String brand;//品牌
private String CPU;//CPU
private String memorySize;//内存大小
private String pixel;//像素大小


public void setBrand(String brand) {
this.brand = brand;
}

public void setCPU(String CPU) {
this.CPU = CPU;
}

public void setMemorySize(String memorySize) {
this.memorySize = memorySize;
}

public void setPixel(String pixel) {
this.pixel = pixel;
}

@Override
public String toString() {
return "品牌:" + brand +
"\nCPU:" + CPU +
"\n内存大小:" + memorySize +
"\n像素大小:" + pixel;
}

我们可以看出,一台手机的构建元素有:品牌、CPU、内存大小以及像素大小,如果还有其他元素,只需要继续添加成员变量即可。
有了手机的构建元素,如何将这些元素拼接成手机,就是Builder要做的事情了:

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class PhoneBuilder {
//构建手机品牌
public abstract void buildBrand(String brand);
//构建手机CPU
public abstract void buildCPU(String cpu);
//构建手机内存
public abstract void buildMemorySize(String memorySize);
//构建手机像素大小
public abstract void buildPixel(String pixel);
//将各个零件进行拼接
public abstract PhoneProduct create();
}

可以看到抽象的Builder类中有各个元素的组装方法,并且最后还有一个将手机进行组装的方法:create()。
接下来我们使用ConcreteBuilder来继承Builder并实现具体的组装细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ConcretePhoneBuilder extends PhoneBuilder {
//商品手机
private PhoneProduct product = new PhoneProduct();

@Override
public void buildBrand(String brand) {
product.setBrand(brand);
}

@Override
public void buildCPU(String cpu) {
product.setCPU(cpu);
}

@Override
public void buildMemorySize(String memorySize) {
product.setMemorySize(memorySize);
}

@Override
public void buildPixel(String pixel) {
product.setPixel(pixel);
}

@Override
public PhoneProduct create() {
return product;
}
}

ConcreteBuilder中包含了每个零件组装和最后拼接过程create()的所有细节。
到这里,我们作为一个手机生产商,已经具备了生产手机的能力,接下来就要接待客户了!
平时大家都说,客户是上帝。今天在这里,也是如此。
所以起到主导作用的,就是我们的客户Director:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PhoneDirector {

private PhoneBuilder builder;

public PhoneDirector(PhoneBuilder builder) {
this.builder = builder;
}

public PhoneProduct constuct(String brand, String cpu, String memorySize, String pixel) {


builder.buildBrand(brand);
builder.buildCPU(cpu);
builder.buildMemorySize(memorySize);
builder.buildPixel(pixel);

return builder.create();
}
}

可以看到Director中持有PhoneBuilder对象,这里的constuct(,,,,)就相当于客户的需求,告诉我们手机的具体配置,接着我们拿着具体需求去ConcreteBuilder中构建具体的手机,并返回给用户。
整个建造者模式就是这样的,接下来我们来做下简单的测试:

1
2
3
4
5
6
7
8
//创建Builder对象
PhoneBuilder miBuilder = new ConcretePhoneBuilder();
//创建管理者
PhoneDirector director = new PhoneDirector(miBuilder);
//生成商品
PhoneProduct product = director.constuct("小米", "骁龙825", "4GB", "1500万像素");
//展示构建结果
textView.setText(product.toString());

首先创建Builder并传递给Director,接下来调用Director的构建方法并传递需求,一台手机就构建出来了。
在这个过程中,管理者Director完全不知手机道构建细节。代码的扩展性以及可读性都有质的提升。

模式变形

在刚刚接触到Builder模式时,我就发现了于我而言,一个非常巨大的应用场景,完美解决一个让我极其难受的问题。
由于篇幅问题,模式变形我决定新起一篇文章中解释。

总结

Demo我已经上传至GitHub,各位看官可自行食用。
上面的例子,仅仅是个例子,看起来似乎仅仅是增加了工作量。
但事实并不是这样的,这种写法的扩展性、可读性都是有很大提升的,希望各位看官都可以理解这种代码思想。
还有一点是,建造者模式与之后要讲到的工厂模式类似,他们都是建造者模式,适用的场景也很相似。
一般来说,如果产品的建造很复杂,那么请用工厂模式;如果产品的建造更复杂,那么请用建造者模式。

感谢

《Android源码设计模式解析与实战》 何红辉、关爱民 著

23种设计模式

发表于 2017-05-31
  1. 单例模式
  2. 建造者模式
  3. 原型模式
  4. 工厂方法模式
  5. 抽象工厂模式
  6. 策略模式
  7. 状态模式
  8. 责任链模式
  9. 解释器模式
  10. 命令模式
  11. 观察者模式
  12. 备忘录模式
  13. 迭代器模式
  14. 模板方法模式
  15. 访问者模式
  16. 中介者模式
  17. 代理模式
  18. 组合模式
  19. 适配器模式
  20. 装饰模式
  21. 享元模式
  22. 外观模式
  23. 桥接模式

参考:《Android源码设计模式解析与实战》 何红辉、关爱民 著

应用最广-单例模式

发表于 2017-05-27

模式介绍

单例模式是应用最广泛的模式之一。
单例模式是为了确保一个类在整个项目中只有一个实例对象。
单例模式最大的优势就是可以避免资源的浪费。
比如访问IO和数据库等资源时就应考虑使用单例模式。

模式特点

  1. 构造方法私有化,使用private来修饰;
  2. 确保对象有且只有一个,尤其是在多线程的环境下;
  3. 通过静态方法或枚举返回已经实例化好的对象。

模式示例

实现单例模式的方式有很多,不过核心是不变的,都要严格遵循单例模式的特点。
下面我们来介绍实现单例的方式:

  1. 饿汉式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class 饿汉式 {
    //自行实例化对象
    private static final 饿汉式 ourInstance = new 饿汉式();
    //通过静态方法返回对象
    public static 饿汉式 getInstance() {
    return ourInstance;
    }
    //构造方法私有化,不能通过new来创建对象
    private 饿汉式() {
    }
    }

    值得一提的是,AndroidStudio在创建类时指定该类为单例的时候,默认就是使用饿汉式:

    饿汉式写起来非常简单、快捷,但是缺点也显而易见:
    在类初始化的时候,对象就已经创建好了。
    如果说我们没有用到该类,就会造成资源的浪费。

  2. 懒汉式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class 最简单的懒汉式 {
    //全项目唯一的对象
    private static 最简单的懒汉式 ourInstance;

    //构造方法私有化
    private 最简单的懒汉式() {

    }

    //通过静态方法来返回对象
    public static 最简单的懒汉式 getInstance() {
    //在调用该方法时进行判空,在对象为null时创建对象
    if (ourInstance == null) {
    ourInstance = new 最简单的懒汉式();
    }
    return ourInstance;
    }
    }

    这就是单例中懒汉式的最基本写法。
    比起饿汉式,最大的优势就是不会造成资源的浪费。因为只有在用到时,才会进行对象的实例化。
    但是就上面的写法而言,还存在一个很致命的问题:
    在多线程同时调用时,会出现多个实例对象的情况。
    Demo里有对应的测试代码,出现的概率很小,但是确实会出现。
    解决这个问题的方式也很简单,为静态方法添加同步锁:

    1
    2
    3
    4
    5
    6
    7
    8
    //通过静态方法来返回对象
    public static synchronized 同步锁的懒汉式 getInstance() {
    //在调用该方法时进行判空,在对象为null时创建对象
    if (ourInstance == null) {
    ourInstance = new 同步锁的懒汉式();
    }
    return ourInstance;
    }

    synchronized就是同步锁的关键字,加上该关键字,代表着该方法同时只能在唯一的一个线程中运行。
    比如当10个线程去调用同步锁的懒汉式.getInstance()时,只有当第1个线程完成访问时,第2个线程才会开始执行该方法。当第1个线程访问完成后,单例对象就已经创建完成,所以第2个线程就会直接返回该对象,不会再去创建,这就保证了线程安全。
    这样确实解决了我们所说的线程安全的问题,但是这种做法明显是低效率的:
    我们的目的是保证项目中有且只有一个对象,上述代码确实实现了这个目的。但是当对象创建成功后,我们希望多线程访问的时候应该是异步高效、同时执行的的,而不是像上面那样队列式的,我要等你用完我才能用。所以就有了双重校验锁的懒汉式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public static 同步锁的懒汉式 getInstance() {
    if (ourInstance == null) {
    synchronized (new Object()) {
    if (ourInstance == null) {
    ourInstance = new 同步锁的懒汉式()
    }
    }
    }
    return ourInstance;
    }

    这种写法可以完美解决多线程效率低下的问题,那么到底是如何解决的?
    双重校验锁指的是会进行两次判空操作:

    1
    ourInstance == null

    一次在同步锁外,一次在同步锁内。
    有的看官就有疑问了:两次判空?
    首先是synchronized关键字,我们删除了方法的同步锁,将其移动到了方法内部,对

    1
    ourInstance = new 同步锁的懒汉式()

    单独加锁。这就代表着我们这个方法本身已经不是线程安全了,会有多个线程同时访问外层的if。如果同步锁内部没有判空,就会有多个线程等待对象创建,就会生成多个实例对象。
    所以双重校验锁的每一步都非常关键,必不可少。
    双重校验锁的写法主要是为了在多线程创建对象时,用同步锁来保证对象的唯一。当对象创建完成后,同步锁外层的判空操作就不成立了,那么会直接返回对象,整个方法就与同步锁无关,多线程访问时也就不需要等待了。
    双重校验锁懒汉式,看起来已经非常完美了!
    但是,很遗憾。
    因为JVM存在指令重排的优化,又会产生新的问题。

    指令重排是JVM为了提高程序运行效率。
    JVM规范规定,指令重排序可以在不影响单线程程序执行结果的情况下改变代码执行顺序。
    该处会产生指令重排的代码是

    1
    ourInstance = new 同步锁的懒汉式();

    这句代码在JVM看来,主要是做了以下三件事情:
    (1)给ourInstance分配内存;
    (2)调用构造方法创建对象,对对象进行初始化;
    (3)将ourInstance对象指向JVM分配的内存空间(此步完成之后,ourInstance就是非null了)。
    因为JVM存在指令重排,所以在不影响最终结果的情况下,JVM会选择性能最优的的顺序执行:
    也就是说,上面三件事情,执行的顺序可能是1-2-3,也有可能是1-3-2。
    1-2-3,1-3-2,有区别吗?
    在结果上来看,没有任何区别。
    但是在多线程的情况下,是有风险的:
    假设线程x的执行顺序是1-3-2,当3执行完成时,ourInstance就已经不为空了,但是2还没有执行完成时,线程y介入了。此时线程y会发现ourInstance已经不为null了,但是其实ourInstance的初始化工作并未完成,这样很明显就会产生异常。
    解决方法也非常简单,利用volatile关键字即可:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class 完美的懒汉式 {
    //全项目唯一的对象
    //volatile关键字,禁止指令重排
    private volatile static 完美的懒汉式 ourInstance;

    //构造方法私有化
    private 完美的懒汉式() {

    }

    //通过静态方法来返回对象
    public static 完美的懒汉式 getInstance() {
    //在调用该方法时进行判空,在对象为null时创建对象
    if (ourInstance == null) {
    synchronized (new Object()) {
    if (ourInstance == null) {
    ourInstance = new 完美的懒汉式();
    }
    }
    }
    return ourInstance;
    }

    上述代码就是一个完美的懒汉式了,利用volatile关键字来禁止JVM的指令重排。

  3. 枚举(Enum)

    1
    2
    3
    4
    5
    6
    7
    8
     public enum 枚举单例 {

    INSTANCE;

    public String getUrl(){
    return "http://www.baidu.com";
    }
    }

    使用起来也非常简单:

    1
    String url = 枚举单例.INSTANCE.getUrl();

    简直完美啊!简单易用,代码清晰!
    但是,很少有人选择用枚举单例。
    可能。。。。。?

总结

简单回顾一下:
单例模式是保证了一个类在一个项目中有且只有一个实例对象。
这样做的目的是为了节省内存的开支。
单例模式的写法主要有:

  • 项目初始化时就创建好的饿汉式
  • 在第一次使用时才进行创建、但要注意线程安全的懒汉式
  • 使用非常简单的枚举
    ##

    感谢

    Jark’s Blog-如何正确地写出单例模式
    《Android源码设计模式解析与实战》 何红辉、关爱民 著

Google I/O 2017 Android

发表于 2017-05-23

Google I/O 2017已经结束了一周了,这篇小文将简单总结一下Android的变化。
不得不说,本次I/O大会,Android从主角完美蜕变成了配角。人工智能成为重心。
参考:Google中文博客

Android O预览版

以流畅度优化为重心的Android最新版本Android O预览版正式发布:
Android O预览版地址
Android O新特性

Project Treble

全新的Android框架,帮助缩短设备制造商升级Android版本所需时间和减少工作量,此项目将从Andorid O开始实施。

Android Go

针对低配置(1GB运行内存以下)的Android系统,为了增加Android系统的市场占有率,最低可支持512M的手机配置。

Kotlin

一门设计精美并得到Google认可的语言,Android将其作为了一级语言。Google认为它可以使Android开发更快、更有趣。附上翻译教程。

AndroidStudio3.0 Canary

AndroidStudio开发工具的又一次革新。本次预览版包含三大主要功能:

  • 全新的应用性能分析工具
  • 完全支持Kotlin语言
  • 加快大规模项目的Gradle构建速度
    具体详情可以看这里。

其他

其他技能的话各位看官可以移步官方博客去了解,中文版的已经出了,而且网络上也已经到处都是译文和总结了。
包括人工智能、VR、Goole Assistant、Google Home产品等等。
作为一名Android小将,本次大会要学习、要适应的东西还是很多的。
共勉。

EditText每4位自动添加空格

发表于 2017-05-19

基本功能

刚拿到需求,很简单的一个功能,二话不说,很快就出来了:

完美!顺利上线!
没过几天领导拿着手机过来说:“这一堆数字在一起看着很费劲,像其他App一样,加个空格吧!“
小Kiss,当即就答应了下来。

拓展功能

下面就来在基本功能上做拓展:每4位,自动添加空格。
看似很小的功能,在开发的过程中,遇到了非常多的问题与难点:

  • EditText输入框监听死循环
  • 输入框中的空格无法删除(删除又添加)
  • 从中间删除一个数字产生的一系列问题
  • 输入框光标位置的控制问题

之前踩坑的过程就不再赘述了,太心酸….

经过一系列实验,最后定下来的思路如下:

  1. 当输入框的内容改变时,就将内容取出拆分为一个一个的字符,在每4位的中间添加空格,最后一个4位不能添加。用这种拼接字符的方法是为了解决当用户删除中间的数字,会导致空格位置错位的问题。
  2. 当用户删除中间的字符时,要记录该动作并且记录光标位置,保证重新排序完成后,光标的位置在应该在的位置。

大概就这2步,就可以实现这个功能,下面一步一来,我们先实现空格的添加,保证内容永远满足4位后一个空格:
下面先看EditText的监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
et_credit_number.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
//获取输入框中的内容,不可以去空格
String etContent = EditTextUtils.getText(et_credit_number);
if (TextUtils.isEmpty(etContent)) {
bt_submit.setEnabled(false);
return;
}
//重新拼接字符串
String newContent = AppUtils.addSpeaceByCredit(etContent);
//如果有改变,则重新填充
//防止EditText无限setText()产生死循环
if (!etContent.equals(newContent)) {
et_credit_number.setText(newContent);
//保证光标在最后,因为每次setText都会导致光标重置
//这样最基本地解决了光标乱跳的问题
et_credit_number.setSelection(newContent.length());
}
//判断是否满足信用卡格式,注意去空格判断
if (MatcheUtils.isCreditNumber(newContent.replaceAll(" ", ""))) {
bt_submit.setEnabled(true);
return;
}
bt_submit.setEnabled(false);
}
});

没有难点,重新拼接字符串我单独封装了出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static String addSpeaceByCredit(String content) {
if (TextUtils.isEmpty(content)) {
return "";
}
//去空格
content = content.replaceAll(" ", "");
if (TextUtils.isEmpty(content)) {
return "";
}
//卡号限制为16位
if (content.length() > 16) {
content = content.substring(0, 16);
}
StringBuilder newString = new StringBuilder();
for (int i = 1; i <= content.length(); i++) {
//当为第4位时,并且不是最后一个第4位时
//拼接字符的同时,拼接一个空格
//如果在最后一个第四位也拼接,会产生空格无法删除的问题
//因为一删除,马上触发输入框改变监听,又重新生成了空格
if (i % 4 == 0 && i != content.length()) {
newString.append(content.charAt(i - 1) + " ");
} else {
//如果不是4位的倍数,则直接拼接字符即可
newString.append(content.charAt(i - 1));

}
}
return newString.toString();
}

这里每一步的含义,我都写了注释,应该问题不大,下面运行一下:

完美!空格正常添加了!
但是光标乱跳的问题,我特地演示了一下。
用字符排序的方式来做这个功能的原因是这个,当用户从中间删除字符时,我们需要将所有添加的空格位置都进行审查,并重新进行空格的添加,所以我认为重新排序字符是非常恰当的一种做法。当然这仅仅是我的愚见,可能有更优的做法。
现在我们就要进行第二步,当用户删除中间字符时,我们要判断用户本次操作是删除字符,并且保存本次删除的光标位置,在删除完成、排序完成之后,将光标移动到保存的光标位置。
思路有了,下面就看最终代码好了。

功能展示


输入框监听的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
et_credit_number.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
//因为重新排序之后setText的存在
//会导致输入框的内容从0开始输入,这里是为了避免这种情况产生一系列问题
if (start == 0 && count > 0) {
return;
}
String editTextContent = EditTextUtils.getText(et_credit_number);
if (TextUtils.isEmpty(editTextContent) || TextUtils.isEmpty(lastString)) {
return;
}
editTextContent = AppUtils.addSpeaceByCredit(editTextContent);
//如果最新的长度 < 上次的长度,代表进行了删除
if (editTextContent.length() <= lastString.length()) {
deleteSelect = start;
} else {
deleteSelect = editTextContent.length();
}
}
@Override
public void afterTextChanged(Editable s) {
//获取输入框中的内容,不可以去空格
String etContent = EditTextUtils.getText(et_credit_number);
if (TextUtils.isEmpty(etContent)) {
bt_submit.setEnabled(false);
return;
}
//重新拼接字符串
String newContent = AppUtils.addSpeaceByCredit(etContent);
//保存本次字符串数据
lastString = newContent;
//如果有改变,则重新填充
//防止EditText无限setText()产生死循环
if (!etContent.equals(newContent)) {
et_credit_number.setText(newContent);
//保证光标的位置
et_credit_number.setSelection(deleteSelect > newContent.length() ? newContent.length() : deleteSelect);
}
//判断是否满足信用卡格式,注意去空格判断
if (MatcheUtils.isCreditNumber(newContent.replaceAll(" ", ""))) {
bt_submit.setEnabled(true);
return;
}
bt_submit.setEnabled(false);
}
});

这边主要利用了onTextChanged()的监听,判断用户操作是删除操作时,保存光标的位置。

小结

项目我已经上传到了我的GitHub,有兴趣的同学可以去参考一下。
这个功能的坑远远超出了我的想象,我才不会说这个项目我就运行了100遍而已!

Android_Handler机制

发表于 2017-05-15

Handler是Android消息通讯当中最常用的方式之一。
本篇小文将会从Handler的源码角度去浅析Handler。

总结

因为Handler这个东西其实大家都会用,源码也多多少少地了解过,所以直接将最关键的话,写到前面,对源码感兴趣的看官可以在看完总结后再往下浏览源码:

  1. 创建Handler

    1
    Handler handler = new Handler();

    在主线程当中,可以直接进行Handler的创建。如果是在子线程当中,在创建之前必须先初始化Looper,否则会RuntimeException:

    1
    2
    3
    4
    Looper.prepare();
    Handler handler = new Handler();
    handler.sendEmptyMessage(8);
    Looper.loop();

    在初始化Looper的同时,一定要调用Looper.loop()来启动循环,否则Handler仍然无法正常接收。
    并且因为Looper.loop()有死循环的存在,Looper.loop()之后的代码将无法执行,所以需要将Looper.loop()放在代码的最后,详情可参考ActivityThread中的main方法创建Looper的流程。
    查看Handler的构造方法可以发现,其实Looper是Handler必要元素,但是在主线程初始化的时候,Looper已经初始化完成,所以无需再创建Looper,但是子线程的Looper需要我们自己初始化。
    并且Looper在每个线程只能存在一个,如果再去手动创建Looper也会抛出RuntimeException。

  2. 发送Handler
    handler的发送有很多方法,包括发送延时Handler、即时Handler、MessageHandler等等,但是查看这些发送方法的源码,就会发现这些发送Message的方法最终都会调用MessageQueue.enqueueMessage()方法。
    这个方法其实就是将我们发送的Message入队到MessageQueue队列中,这样,我们的消息就已经发送成功,等待执行了。

  3. 取出Handler
    在Looper.prepare()的同时,总会执行looper.loop()语句与之对应。
    查看loop()源码会发现,这个方法中有一个for(;;)的死循环,会无限执行MessageQueue.next(),而MessageQueue就是我们上一步将Meesage入队的对象。
    也就是说在创建Looper时,就会启动MessageQueue的无限遍历。如果MessageQueue为空,Looper.loop()就会进入休眠,直到再有Message插入到MessageQueue中。
    如果取到Message则会调用message.target.dispatchMessage(),将消息分发给对应的Handler。
  4. 如何在loop()休眠之后唤醒loop()?
    在Meesage入队的时候,也就是执行MessageQueue.enqueueMessage()方法时,enqueueMessage()有一个nativeWeak()的native方法,如果有消息进入,并且Looper是休眠状态,则会执行该方法唤醒Looper:

    1
    2
    3
    4
    // We can assume mPtr != 0 because mQuitting is false.
    if (needWake) {
    nativeWake(mPtr);
    }
  5. 整体流程
    先调用Looper.prepare()创建Looper,在创建的同时会自动调用Looper.loop()执行死循环loop()。注意Looper.loop()一定放到代码的最后一行。
    死循环中会执行MessageQueue.next()方法去取出队列中的消息,当消息为空时,MessageQueue.next()方法中会执行nativePollOnce()的native方法休眠Looper.loop()死循环。当有新的消息插入到MessageQueue中,也就是调用MessageQueue.enqueueMessage()方法,这个方法当中会判断Looper是否是休眠状态,如果是休眠状态会执行nativeWeak()的native方法来唤醒Looper()。

Handler的使用

  1. 主线程
    在主线程当中,Handler可以作为一个成员变量直接进行创建:

    1
    2
    3
    4
    5
    6
    7
     //注意,Handler属于android.os包
    private Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
    System.out.println("主线程的Handler");
    }
    };

    接着我们试着发送Handler:

    1
    2
    3
    4
    5
    6
    7
    8
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    //发送一个空Handler,what为888,延迟3秒到达
    handler.sendEmptyMessageDelayed(888,3000);
    }

    发送Handler有很多方法:发送空Message、发送延迟的空Message、发送Message、发送延迟的Message等:

    接着我们运行项目,就会发现3s后控制台有Log的打印。

  2. 子线程
    在子线程中创建Handler的流程,和在主线程基本一致,只是多了一步而已,但这一步非常关键。
    我们先按照主线程的步骤,看看会有什么问题。
    在子线程中创建Handler:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    new Thread(){
    @Override
    public void run() {
    super.run();
    Handler handler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
    System.out.println("子线程的Handler");
    }
    };
    }
    }.start();

    接着我们运行项目,会发现项目崩溃了:

    1
    2
    3
    4
    java.lang.RuntimeException: Can''t create handler inside thread that has not called Looper.prepare()
    at android.os.Handler.<init>(Handler.java:200)
    at android.os.Handler.<init>(Handler.java:114)
    at com.my.oo.MainActivity$2$1.<init>(MainActivity.java:0)

    很明显,错误信息说的是在当前线程中创建Handler失败因为没有执行Looper.prepare()。
    那我们按照错误原因,在创建Handler之前,加上Looper.prepare():

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //子线程
    new Thread(){
    @Override
    public void run() {
    super.run();
    //添加Looper.prepare();
    Looper.prepare();
    //之后再创建Handler
    Handler handler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
    System.out.println("子线程的Handler");
    }
    };
    //发送消息
    handler.sendEmptyMessage(111);
    }
    }.start();

    这次再运行项目,我们就会发现项目正常运行没有问题。但是发送Handler仍然无法接收,那是因为我们没有启动Looper的遍历:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //子线程
    new Thread(){
    @Override
    public void run() {
    super.run();
    //添加Looper.prepare();
    Looper.prepare();
    //之后再创建Handler
    Handler handler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
    System.out.println("子线程的Handler");
    }
    };
    //发送消息
    handler.sendEmptyMessage(111);
    //启动Looper的遍历功能
    Looper.loop();
    }
    }.start();

    这里一定要注意,Looper.loop()必须放到代码的最后。因为Looper.loop()中有死循环,会导致之后的代码无法执行。这里可以等到查看主线程创建过程的源码时证实。

  3. 使用总结
    Handler的使用就是这么简单,要注意的就是子线程当中使用Handler时,一定要先调用Looper.prepare(),最后调用Looper.loop(),否则项目会崩溃或无法接收Handler。至于为什么会这样,我们在源码里面找原因。

源码解析

  1. Handler创建
    我们先看Handler的构造方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public Handler(Callback callback, boolean async) {
    //....省略部分代码
    mLooper = Looper.myLooper();
    if (mLooper == null) {
    throw new RuntimeException(
    "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
    }

    我们可以看到,Handler的构造方法当中是去获取了一个叫做Looper的类对象,如果该对象为空,就会抛出刚才我们上面发生的异常。所以我们需要在创建Handler之前,一定要先执行Looper.prepare()。
    那么问题来了,为什么主线程就不需要执行Looper.prepare()就可以直接创建Handler呢?
    我们可以随意根据代码猜测一下:
    这里Handler的构造方法的代码已经很明显了,Looper类是必要的,那么主线程可以成功创建Handler,是不是就代表着主线程的Looper不为空呢?

    是不是主线程在初始化的时候Looper也跟着初始化了呢!?带着看破一切(瞎猜)的思路,我们来看主线程的初始化源码。
    经过了长时间的Google,我们知道了主线程类叫做:ActivityThread。
    该类当中有main方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public static void main(String[] args) {
    //....省略部分代码
    Looper.prepareMainLooper();
    ActivityThread thread = new ActivityThread();
    thread.attach(false);
    if (sMainThreadHandler == null) {
    sMainThreadHandler = thread.getHandler();
    }
    if (false) {
    Looper.myLooper().setMessageLogging(new
    LogPrinter(Log.DEBUG, "ActivityThread"));
    }
    // End of event ActivityThreadMain.
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    Looper.loop();
    throw new RuntimeException("Main thread loop unexpectedly exited");
    }

    我们很自然(惊奇)地发现,在主线程创建的过程中,果然(真的)有与Looper类相关的内容。
    这里有重点(敲黑板):之前说的Looper.loop()后的代码不会执行,这里得到了证实:

    1
    2
    Looper.loop();
    throw new RuntimeException("Main thread loop unexpectedly exited");

    下面异常的意思是:主线程的Looper意外退出。
    也就是当Looper.loop()执行失败的意思,但是当Looper.loop()执行成功时,是不会执行下面的代码的!因为Looper.loop()必须放到方法的最后,否则会导致后面的代码无法执行。
    好的,接着往下看,点进prepareMainLooper()会发现,其实内部就是调用了prepare():

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
    if (sMainLooper != null) {
    throw new IllegalStateException("The main Looper has already been prepared.");
    }
    sMainLooper = myLooper();
    }
    }

    所以到现在,我们解决了我们的第一个问题:为什么主线程当中不需要执行Looper.prepare()。
    接着,我们去浏览Looper.prepare():

    1
    2
    3
    4
    5
    6
    7
    private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
    throw new RuntimeException("Only one Looper may be created per thread");
    }
    //new Looper()并设置给当前线程
    sThreadLocal.set(new Looper(quitAllowed));
    }

    没什么东西,前面对线程当中的Looper进行了判空,如果不为空则会抛出RuntimeEception。
    这也就是说,每个线程当中只能有一个Looper,当你尝试去创建第二时,就会发生异常,所以Looper.prepare()每个线程中只能调用一次。
    后面则new了Looper并且设置给当前线程。
    new Looper()中初始化了MessageQueue:

    1
    2
    3
    4
    private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
    }

    到这里,Handler的创建就完成了!

  2. Handler发送Meesage
    Handler的发送方法有很多,包括发送延时Handler、及时Handler、空Hanlder等等。
    查看源码会发现,所有发送方法最后调用的都是同一个方法:MessageQueue的enqueueMessage()。
    有的看官就会问:Handler中怎么会有MeesageQueue?
    这个在上面new Looper()的源码中已经体现了:
    new Handler()中new Looper(),
    new Looper()中new MessageQueue()。
    所以其实初始化Handler的同时,Looper和MeesageQueue都已经初始化完成了。
    下面我们来看消息入队方法MessageQueue.enqueueMessage()的全部源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    boolean enqueueMessage(Message msg, long when) {
    //Meesage是否可用
    //这里的msg.target指的就是发送该Message的Handler
    if (msg.target == null) {
    throw new IllegalArgumentException("Message must have a target.");
    }
    if (msg.isInUse()) {
    throw new IllegalStateException(msg + " This message is already in use.");
    }
    //同步锁
    synchronized (this) {
    //判断是否调用了quit()方法,即取消信息
    //如果调用了,则其实Handler的Looper已经销毁,无法发送消息
    if (mQuitting) {
    IllegalStateException e = new IllegalStateException(
    msg.target + " sending message to a Handler on a dead thread");
    Log.w(TAG, e.getMessage(), e);
    msg.recycle();
    return false;
    }

    //将消息添加到MessageQueue的具体操作
    //每来一个新的消息,就会按照延迟时间的先后重新进行排序
    msg.markInUse();
    msg.when = when;
    Message p = mMessages;
    boolean needWake;
    if (p == null || when == 0 || when < p.when) {
    // New head, wake up the event queue if blocked.
    msg.next = p;
    mMessages = msg;
    needWake = mBlocked;
    } else {
    Message prev;
    for (;;) {
    prev = p;
    p = p.next;
    if (p == null || when < p.when) {
    break;
    }
    if (needWake && p.isAsynchronous()) {
    needWake = false;
    }
    }
    msg.next = p; // invariant: p == prev.next
    prev.next = msg;
    }

    //如果Looper.loop()是休眠状态
    //则调用native方法唤醒loop()
    //---重点---Looper的唤醒
    if (needWake) {
    nativeWake(mPtr);
    }
    }
    return true;
    }

    将Message入队到MeesageQueue的核心代码,就是这些。
    根据注释也基本能理解该方法的作用。

  3. 取出Message
    取出Meesage想必大家都知道在哪里取出:Looper.loop():

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    public static void loop() {
    //Looper的判空
    final Looper me = myLooper();
    if (me == null) {
    throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;
    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();
    //取出Message,死循环
    for (;;) {
    //取出Meesage的核心代码
    //在当next()返回为空时,next()中会休眠loop()
    Message msg = queue.next(); // might block
    if (msg == null) {
    // No message indicates that the message queue is quitting.
    return;
    }
    // This must be in a local variable, in case a UI event sets the logger
    final Printer logging = me.mLogging;
    if (logging != null) {
    logging.println(">>>>> Dispatching to " + msg.target + " " +
    msg.callback + ": " + msg.what);
    }
    final long traceTag = me.mTraceTag;
    if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
    Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
    }
    try {
    msg.target.dispatchMessage(msg);
    } finally {
    if (traceTag != 0) {
    Trace.traceEnd(traceTag);
    }
    }
    if (logging != null) {
    logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
    }
    // Make sure that during the course of dispatching the
    // identity of the thread wasn't corrupted.
    final long newIdent = Binder.clearCallingIdentity();
    if (ident != newIdent) {
    Log.wtf(TAG, "Thread identity changed from 0x"
    + Long.toHexString(ident) + " to 0x"
    + Long.toHexString(newIdent) + " while dispatching to "
    + msg.target.getClass().getName() + " "
    + msg.callback + " what=" + msg.what);
    }
    msg.recycleUnchecked();
    }
    }

    Looper.loop()的核心就是取出Message,而取出Message的核心就是MeesageQueue.next():

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
     Message next() {
    // Return here if the message loop has already quit and been disposed.
    // This can happen if the application tries to restart a looper after quit
    // which is not supported.
    final long ptr = mPtr;
    if (ptr == 0) {
    return null;
    }
    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (;;) {
    if (nextPollTimeoutMillis != 0) {
    Binder.flushPendingCommands();
    }
    //唤醒Looper.loop()的native方法
    nativePollOnce(ptr, nextPollTimeoutMillis);
    synchronized (this) {
    // Try to retrieve the next message. Return if found.
    final long now = SystemClock.uptimeMillis();
    Message prevMsg = null;
    Message msg = mMessages;
    if (msg != null && msg.target == null) {
    // Stalled by a barrier. Find the next asynchronous message in the
    do {
    prevMsg = msg;
    msg = msg.next;
    } while (msg != null && !msg.isAsynchronous());
    }
    if (msg != null) {
    if (now < msg.when) {
    // Next message is not ready. Set a timeout to wake up when it
    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.M
    } else {
    // Got a message.
    mBlocked = false;
    if (prevMsg != null) {
    prevMsg.next = msg.next;
    } else {
    mMessages = msg.next;
    }
    msg.next = null;
    if (DEBUG) Log.v(TAG, "Returning message: " + msg);
    msg.markInUse();
    return msg;
    }
    } else {
    // No more messages.
    nextPollTimeoutMillis = -1;
    }
    // Process the quit message now that all pending messages have been hand
    if (mQuitting) {
    dispose();
    return null;
    }
    // If first time idle, then get the number of idlers to run.
    // Idle handles only run if the queue is empty or if the first message
    // in the queue (possibly a barrier) is due to be handled in the future.
    if (pendingIdleHandlerCount < 0
    && (mMessages == null || now < mMessages.when)) {
    pendingIdleHandlerCount = mIdleHandlers.size();
    }
    if (pendingIdleHandlerCount <= 0) {
    // No idle handlers to run. Loop and wait some more.
    mBlocked = true;
    continue;
    }
    if (mPendingIdleHandlers == null) {
    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCo
    }
    mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
    }
    // Run the idle handlers.
    // We only ever reach this code block during the first iteration.
    for (int i = 0; i < pendingIdleHandlerCount; i++) {
    final IdleHandler idler = mPendingIdleHandlers[i];
    mPendingIdleHandlers[i] = null; // release the reference to the handler
    boolean keep = false;
    try {
    keep = idler.queueIdle();
    } catch (Throwable t) {
    Log.wtf(TAG, "IdleHandler threw exception", t);
    }
    if (!keep) {
    synchronized (this) {
    mIdleHandlers.remove(idler);
    }
    }
    }
    // Reset the idle handler count to 0 so we do not run them again.
    pendingIdleHandlerCount = 0;
    // While calling an idle handler, a new message could have been delivered
    // so go back and look again for a pending message without waiting.
    nextPollTimeoutMillis = 0;
    }
    }

    取出Meesage的代码有些多,大部分都是一些优化逻辑:next() 方法还做了其他一些事情,这些其它事情是为了提高系统效果,利用消息队列在空闲时通过 idle handler 做一些事情,比如 gc 等等。

结语

到这里Handler源码的浅析就结束了,总结在最上方,建议各位看官再去看一下总结加深印象。

Android权限管理简单描述

发表于 2017-05-09

首先安利一个大家都知道的事情,Google推出了Android开发文档中文版。
还有谷歌开发者中文博客。
好的,进入正题:
说到权限变化大家马上就想到Android API23(6.0)之后权限系统大改。当build.gradle中targetSdkVersion设置小于23时,会继续引用旧版本权限管理机制,当targetSdkVersion大于等于23时,则会使用新的权限管理。

targetSdkVersion < 23

当targetSdkVersion小于23时,你的项目会继续使用旧版本的权限机制:

  1. 用户在安装时获取到所有权限,在使用权限时无需进行预判断。
  2. 虽然使用旧版的权限机制,但是在设置-App详情中也可以找到权限管理将其关闭。如果用户手动来到设置将权限关闭,我们的项目在用到该权限时会发生Crash,所以如果使用低版本权限管理,请将需要用到权限的地方try-catch起来。
  3. 那些低版本权限提示弹窗都是手机厂商自定义的。他们拥有系统权限,在检测到你的App使用权限时会提示用户权限授权,如果用户拒绝则替用户到设置中心关闭权限。
  4. 综上,不想使用最新的权限管理,则将targetSdkVersion设置为小于23即可,并且将用到权限的代码try-catch起来。这里需要知道不是所有的权限都可以关闭的,只有涉及用户隐私的权限才会在设置中展示出来,方便用户进行手动关闭。

targetSdkVersion >= 23

  1. 将权限分为了普通权限和隐私权限,普通权限在清单文件中声明则直接获取。隐私权限也需要在清单文件中声明,但是在安装完成后,所有的隐私权限都为拒绝状态,需要在用到隐私权限时判断权限是否开启,否则项目直接发生Crash。
  2. 第一次使用隐私权限时直接弹出系统的权限授权弹窗,用户授权则永久授权,拒绝则无法使用此权限。第二次或者之后用到此权限我们需要做一个自己的弹窗来描述该权限的作用,然后关闭我们自己的弹窗后会弹出系统的权限授权弹窗,此时弹窗上会有不再提醒的提示文字,如果用户勾选了不再提醒则再也不会提醒并且权限关闭。
  3. 检测权限关闭并且勾选了不再提醒我们可以提示用户到设置中心中手动打开权限。用户如果打开则永久授权(当然还是可以再关闭的)。
  4. 权限组的概念也需要知道,比如获取写文件权限时,用户授权则会同时获取读文件的权限,因为他们属于同一个权限组。
  5. 综上,新版本权限的适配也很简单,主要工作是在所有用到隐私权限的地方添加权限是否授权的判断、后续的一些判断细节以及回调。Google写的权限工具easypermissions是我在权限改版时用的,还是非常好用的。demo可参考官方demo。

小结

推荐各位看官最好都及时地进行权限管理的高版本适配,毕竟用户对权限很敏感,这是一个趋势。

理解面向对象

发表于 2017-05-08

面向对象是一种软件开发的方法,同类的还有面向过程。
面向对象指的是在程序设计中采用Java的封装、继承、多态、六大原则特性来设计代码。
它其实考验的是你审视代码的角度,运用这些特性,可以写出让人赏心悦目、简单易懂的代码。
不运用这些特性当然也可以进行开发。不过代码的可读性、扩展性、灵活性等会大大下降,冗余度、维护成本等会大大上升。

封装

  1. 概念
    在Java中,封装就是将一些通用、常用的功能方法写到一个新类中,那么当我们用到这些功能时,直接去调用这个新类中的方法即可。这就像是有一个人A,他拥有一些技能,当我用到这些技能时,不需要自己去学习这些技能,只需要去找A即可。
  2. 优点
    提高代码的重用,减少重复代码,提高代码的可读性、方便理解。而且封装的思想也对应了Java中一处编程,到处执行的思想。
  3. 实例
    实例就不用多说了,平时写代码我们总会自己创建一个utils包,存放一些自己或者别人写的utils类。

继承

  1. 概念
    继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力(来自百度百科)。
    这种官方语言太难讲,而且各位看官也看着费劲。我还是直接说自己的理解吧。
    首先继承的含义,就是一直我们口中所说的父类(基类)和子类。子类通过关键字extends继承父类,就可以拥有父类的非私有的属性和方法。
    在Java中,继承是单一继承+多层继承的。
  2. 优点
    提高代码的重用,减少重复的代码。增加了软件开发的灵活性。
  3. 缺点
    由于多层继承的存在,所以无限使用继承特性的话,会造成子类的过度冗余。

多态

  1. 概念
    多态指的是同一个方法,会因为对象的不同导致不同的结果。
    没错,就是这样!

    多态的三要素一定要知道。这个东西理解了自然就记住了。
    继承、重写、父类的引用指向子类的对象。
    具体含义,还是在后面举个栗子来解释一下。
  2. 优点
    增加了软件开发的灵活性,简化了编程和修改过程。
  3. 实例
    首先我们定义了一个汽车接口Car,接口中有一个方法用来获取车的类型:

    1
    2
    3
    4
    public interface Car {

    String getCarType();
    }

    接下来,我们创建了兰博基尼,以及五菱宏光实现了这个接口。
    五菱宏光:

    1
    2
    3
    4
    5
    6
    public class WuLingHongGuang implements Car {
    @Override
    public String getCarType() {
    return "五菱宏光";
    }
    }

    兰博基尼:

    1
    2
    3
    4
    5
    6
    public class LanBoJiNi implements Car {
    @Override
    public String getCarType() {
    return "兰博基尼";
    }
    }

    接下来,我们利用多态的特性,来创建并执行接下来的代码:

    1
    2
    3
    4
    5
    6
    7
    public static void main(String[] args){
    Car car1 = new LanBoJiNi();
    Car car2 = new WuLingHongGuang();

    System.out.println("车1的类型:"+car1.getCarType());
    System.out.println("车2的类型:"+car2.getCarType());
    }

    可以看到控制台的结果:

    1
    2
    车1的类型:兰博基尼
    车2的类型:五菱宏光
  4. 总结
    通过实例,再结合多态三要素:继承、重写、父类的引用指向子类的对象。
    兰博基尼和五菱宏光实现了Car接口(继承),重写了Car中的getCarType()(重写),接下来最关键的要素,我们用父类的引用,创建子类的对象。

    那么接下来,当你去调用getCarType()时,Java会首先调用子类中的getCarType(),而不是父类Car中的。
    其实这个实例,并不能帮你很好地理解多态。它只是很生硬地运用了多态的特性。在项目中运用多态非常的重要,这个需要自己实战才能好好理解。

六大原则

六大原则包括:单一职责、开闭、里氏替换、依赖倒置、接口隔离、迪米特。接下来我们一个一个来理解这些原则的思想。

单一职责原则(Single Responsibility Principle)

  1. 概念
    就一个类而言,应该仅有一个引起它变化的原因。
    非常容易理解的一个原则,并且非常重要!但这个原则是一个备受争议的原则,和别人争论这个原则,是屡试不爽的。

    因为单一职责原则划分界限并不总是那么清晰,更多的时候是根据个人经验来界定的。

开闭原则(Open Close Principle)

  1. 概念
    就一个类或方法而言,应该对于扩展是开放的,对于修改是关闭的。
    软件也有自己的生命周期,越往后迭代,代码越多,冗余度也会随之提升,维护成本也就越来越高,可能一次不经意地bug修改就会破坏之前已经通过测试的代码。因此,当软件需要变化时,我们应该通过扩展的方式去实现,而不是通过修改原有代码来实现。
    当然,这是理想愿景,在实际开发中往往是扩展和修改同时存在的,因为再好的代码,终有一天也会有无法适应的场景出现。所以,我们要做的,就是在开发新东西的时候,尽可能地考虑多的场景,尽可能降低修改的可能性。并且当我们发现代码有“腐朽”的味道时,应该尽早地进行代码重构,使代码恢复到正常的“进化”过程。

里氏替换原则(Liskov Substitution Principle)

  1. 概念
    所有引用父类的地方,都可以透明的传递子类对象。
    这个原则简直就是多态的完美体现。
    这个原则强调的是运用多态时,应该注子类的适配,使之无论传递任何子类对象,都能完美适应父类引用,不会产生异常。
  2. 实例
    我们继续引用多态的那个实例,在那个实例之上做些修改。
    现在我们是汽车制造商,你只需要告诉我品牌,我就可以生产出对应品牌的车。
    我们目前只能生产兰博基尼和五菱宏光,那么接下来我们改变一下main方法,生产一下这2辆车:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public static void main(String[] args) {
    //创建兰博基尼
    System.out.println("生产了一辆:" + createCar(new LanBoJiNi()));
    //创建五菱宏光
    System.out.println("生产了一辆:" + createCar(new WuLingHongGuang()));
    }


    private static String createCar(Car car) {
    return car.getCarType();
    }

    上面当中,我们通过createCar(Car car)来创建了兰博基尼和五菱宏光,虽然createCar()要求的参数是Car,但是我们传入了子类对象兰博基尼和五菱宏光。并且这2个类都做了很好的适配(一个栗子而已,大家就认为其实我的兰博基尼其实是经过500道独特工序才制造出来的,五菱宏光是另外的500道工序),无论我传入谁,都可以完美生产,不会产生异常。这就是里氏替换原则!

依赖倒置原则(Dependence Inversion Principle)

  1. 概念
    依赖倒置原则指代了一种特定的解耦形式,使得高层次的模块不依赖低层次的模块去实现细节。主要关键点有以下3点:
  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象。
  • 抽象不应该依赖细节。
  • 细节应该依赖细节。

    在Java语言中,抽象指的就是接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或者继承抽象类而产生的类就是细节,可以直接被实例化。高层模块就是抽象,底层模块就是具体实现类。一句话概括的话就是:面向接口编程。面向接口编程是面向对象的精髓之一。

接口隔离原则(Interface Segregation Principles)

  1. 概念
    类不应该依赖它不需要的接口。
    另一种定义是:类依赖的接口都应该是最小单位。
    那么接口隔离原则其实就是要求我们将庞大、臃肿的接口按照某种规则,将其拆封成更小的、更具体的接口,这样客户端只需要依赖它需要的接口即可。
    接口隔离原则的目的就是使系统解耦,从而更容易进行重构、更改等操作。

迪米特原则(Law of Demeter)

  1. 概念
    一个对象应该对其他对象有最少的依赖。
    通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,类的内部如何实现与调用者或者依赖者没关系,调用者或依赖者只需要知道它需要的方法即可,其他的一概不用管。
    如果两个对象的关系过于密切,那么当一个对象发生变化时,另一个对象就会产生不可预估的风险。

小结

在应用的开发过程中,最难的不是完成应用的开发工作,而是在后续的升级、维护过程中让应用系统能够拥抱变化。拥抱变化也就意味着在满足需求且不破坏系统稳定性的前提下保持高扩展性、高内聚、低耦合,在经历了各版本的变更之后依然保持清晰、灵活、稳定的系统架构。
当然这是一个理想的情况,但我们必须要朝着这个方向去努力,那么遵循面向对象思想就是我们走向理想的第一步。

感谢

  1. 《Android源码设计模式解析与实战》 何红辉、关爱民 著
  2. 百度百科

有任何问题都可以联系我:mengyuanzz@126.com

123
孟远

孟远

一名Android开发小将

22 日志
8 标签
GitHub E-Mail 知乎 简书
© 2020 孟远
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4