0%

月亮与六便士

忘了何时开始关注《月亮与六便士》,可能是看了某篇荐书类的文章,直到前两天看完,最大的感受便是惊叹作者叙述技法的娴熟,读来行云流水,自有一气呵成之快感。

看到很多人喷译者的导读过长,甚是不能理解。难道不看导读的你真能看懂作者想要表达的现实主义吗?你能理解作者为何取名为《月亮与六便士》?你能明白小说的结尾说的那段话又有什么深意?

起初读起前几章,似乎乏味到想弃读;转折点大概是在斯特里克兰突然离开伦敦之时,此时才明白他是故事的主人公,故而随着作者叙事技法的高超,越来越精彩,一切似乎都是水到渠成,至少在阅读量有限的我来说是这样的。

作者对于人物的刻画相当擅长,如第十一章中:“我发现她的举手投足颇有自相矛盾之处,这让我感到大惑不解。她确实非常悲伤,但为了激起我的同情,她竟然会将悲伤表演给我看。她的痛哭流涕显然是经过精心准备的,因为她在身边放了大量的手帕,我特别佩服她的深谋远虑,但回想起来,这也许会让她的眼泪没有那么动人。我无法确定她希望她的丈夫回家,是因为还爱着她丈夫,还是因为害怕人言物议。我忍不住怀疑在她支离破碎的心里,除了夫妻反目造成的酸楚,是否也混杂着虚荣心受损带来的痛苦——这种动机在年轻的我看来是很可耻的。那时候我尚未明白人性是多么的悖谬,我还不知道真挚诚恳底下也许埋藏着矫揉造作,高风亮节背后可能隐匿着卑鄙无耻,也不知道无赖恶棍心里或许存留着良善之意。”本来还对她怀有同情心,在作者的一段心里刻画之后,让人对她的遭遇有种“丑人多作怪”的“愉悦”。

书中金句不断,有的犀利,有的则直戳心底,如:“我们胃口都很好,我是因为年纪尚轻,他则是因为毫无良心。”这里表面上展现了斯特里克兰的“毫无良心”,其实看到最后时并非是这样。

再比如:“有的人也号称他们不在意别人的看法,但他们多半是在自己骗自己。总的来说,这些人只有在相信没人能发现他们的逾规越矩之处时才敢为所欲为。他们顶多就是因为有了几个亲朋好友的赞许,愿意去做一些与大多数人的观点相悖的事情。假如你的离经叛道无非是你这类人的惯用伎俩,那么在世人面前表现得离经叛道并不是很困难的事情。这会让你对自己肃然起敬。你既可以标榜自己勇气过人,又无须冒什么实际的危险。但渴望得到认可也许是文明人最根深蒂固的本能。哪怕是最不守妇道的女人,若是舆论纷纷指责她伤风败俗,她也会赶紧跑去求某个德高望重的人士为她主持公道。如果有人告诉我他们完全无视别人的看法,那我是不相信的。这是一种无知的虚张声势。这些人的意思无非是,他们不怕由于一些微不足道的过失而受到指责,因为他们自信没有人能发现。”这时我们来思考一个问题:人们嘴上说不在意别人的看法,还是在窃喜那些逾规越矩的行为没有被发现而已?

再比如:“现在我清楚地认识到,卑鄙和高尚、凶恶和仁慈、憎恨和爱恋是能够并存于同一颗人类的心灵的。”人是多么复杂的生物,在某种情况下是那么的卑鄙、丑陋;而在另一种处境下又是那么的善良和仁慈…

最后我想聊聊书名《月亮与六便士》,一开始并不理解它的含义,通过导读了解作者的时代背景后才明白,毛姆开始的著作总是受到同行的排挤和批评,对热爱文学创作的他来说,经济的困窘或可一笑置之,心血长久无人问津却会造成致命的信心动摇。但优秀的作品总不会一直黯淡下去,转机很快在大西洋彼岸出现:其美国版在同年7月推出,首印五千本旋即售罄,到年底竟然卖掉将近十万册,进而让沉寂数年的《人性的枷锁》重见天日,并最终在文学史上奠定了无可撼动的经典地位。就我个人而言,了解到上面的背景之后,不经起了一身鸡皮疙瘩,或许是为了优秀作品的光芒再现,或许是同情毛姆的创作经历。《月亮与六便士》中的月亮象征着崇高的理想追求和美妙的精神境界,六便士这种小面额的硬币代表着世俗的鸡虫得失与蝇头小利。至于精神与物质之间如何取舍,作者并没有给出一个结论,其实也并不需要什么结论,这完全取决于你如何看待生活的意义,取决于你认为你应该对社会做出什么贡献,应该对自己有什么要求。

所以小说的最后作者突兀的提到自己的叔叔亨利当牧师是说的一句话:魔鬼总是随心所欲的引用经文,他记得从前一个先令就能买到十三只上等的牡蛎。这里暂且不谈其引申的含义,就那句经文而言,最有可能的就是“你们不要论断人,免得你们被论断”,也就是不要轻易的judge别人,因为人和人的相互了解往往肤浅、局限而片面,能够做出公正的评判只有全知全能的上帝——假如这样的上帝果真存在的话。

1. Base64编码算法

Base64只是一种编码方式,并不是一种加密算法,不要使用Base64来加密数据。

Base64编码算法是一种用64个字符,在计算机网络发展的早期,由于“历史原因”,电子邮件不支持非ASCII码字符,如果要传送的电子邮件带有非ASCII码字符(诸如中文)或者图片,用户收到的电子邮件将会是一堆乱码,因此发明了Base64编码算法。

在加解密算法中,原始的数据和加密后的数据一般也是二进制数据,为了不传输出错,方便保存或者调试代码,一般需要对加密后的数据进行base64编码。

Android提供了Base64编码的工具类android.util.Base64

Base64编码

2. 随机数生成器

在Android加密算法中需要随机数时要使用SecureRandom来获取随机数,如下图示:

随机数生成器

注意不要给SecureRandom设置种子。调用seeded constructor或者setSeed(byte[])是不安全的。SecureRandom()默认使用的是dev/urandom作为种子产生器,这个种子是不可预测的。

开发者建议

  • 不要使用Random类来获取随机数
  • 在使用SecureRandom时候,不要设置种子。使用以下函数设置种子都是有风险的

SecureRandom

3. Hash算法

Hash算法是指任意长度的字符串输入,此算法能给出固定n比特的字符串输出,输出的字符串一般称为Hash值。

两个特点

  1. 抗碰撞性

    抗碰撞性使Hash算法对原始输入的任意一点更改,都会导致产生不同的Hash值,因此Hash算法可以用来检验数据的完整性。

  2. 不可逆性

    不可逆的特性使Hash算法成为一种单向密码体制,只能加密不能解密,可以用来加密用户的登录密码等凭证。

开发者建议

  1. 建议使用SHA-256、SHA-3算法,不建议使用MD2、MD4、MD5、SHA-1、RIPEMD算法来加密用户密码等敏感信息。这一类算法已经有很多破解办法,例如md5算法,网上有很多查询的字典库,给出md5值,可以查到加密前的数据。
  2. 不要使用哈希函数做为对称加密算法的签名,当多个字符串串接后再做hash,要非常当心。实际开发过程中经常会对url的各个参数,做词典排序,然后取参数名和值串接后加上某个SECRET字符串,计算出hash值,作为此URL的签名。

4. 消息认证算法

要确保加密的消息不是别人伪造的,需要提供一个消息认证码(MAC,Message authentication code)。消息认证码是带密钥的hash函数,基于密钥和hash函数。消息发送者使用MAC算法计算出消息的MAC值,追加到消息后面一起发送给接收者。接收者收到消息后,用相同的MAC算法计算接收到消息MAC值,并与接收到的MAC值对比是否一样。

5. 对称加密算法

在对称加密算法中,使用的密钥只有一个,发收信双方都使用这个密钥对数据进行加密和解密,
这就要求解密方事先必须知道加密密钥。该算法的缺点是,如果一旦密钥泄漏,那么加密的内容将都不可信了。

开发者建议

建议使用AES算法,DES默认的是56位的加密密钥,已经不安全,不建议使用。

ECB

注意加密模式不要使用ECB模式。ECB模式不安全,说明问题的经典的三张图片。

AndroidAES

Android 提供的AES加密算法API默认使用的是ECB模式,所以要显式指定加密算法为:CBC或CFB模式,可带上PKCS5Padding填充。AES密钥长度最少是128位,推荐使用256位。

6. 非对称加密

非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)。公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密(这个过程可以做数字签名)。非对称加密主要使用的是RSA算法

开发者建议

  1. 注意密钥长度不要低于512位,建议使用2048位的密钥长度。 使用RSA进行数字签名的算法。
    RSA
  2. 使用RSA算法做加密,RSA加密算法应使用Cipher.getInstanceRSA/ECB/OAEPWithSHA256AndMGF1Padding),否则会存在重放攻击的风险。
    RSA2

7. 加密算法PBE

PBE是一种基于口令的加密算法,其特点是使用口令代替了密钥,而口令由用户自己掌管,采用随机数杂凑多重加密等方法保证数据的安全性。

开发者建议

  1. 使用基于口令的加密算法PBE时,生成密钥时要加盐,盐的取值最好来自SecureRandom,并指定迭代次数。
    PBE

8. 加密和解密

加密要用公钥 (n,e)

  • 假设鲍勃要向爱丽丝发送加密信息m,他就要用爱丽丝的公钥 (n,e) 对m进行加密。这里需要注意,m必须是整数(字符串可以取ascii值或unicode值),且m必须小于n。
  • 所谓”加密”,就是算出下式的c:me ≡ c (mod n),于是,c等于2790,鲍勃就把2790发给了爱丽丝。

解密要用私钥(n,d)

  • 爱丽丝拿到鲍勃发来的2790以后,就用自己的私钥(3233, 2753) 进行解密。可以证明,下面的等式一定成立:cd ≡ m (mod n)
  • 也就是说,c的d次方除以n的余数为m。现在,c等于2790,私钥是(3233, 2753),那么,爱丽丝算出:27902753 ≡ 65 (mod 3233)
  • 因此,爱丽丝知道了鲍勃加密前的原文就是65。

公钥(n,e) 只能加密小于n的整数m,那么如果要加密大于n的整数,该怎么办?

  • 一种是把长信息分割成若干段短消息,每段分别加密;
  • 另一种是先选择一种”对称性加密算法”(比如DES),用这种算法的密钥加密信息,再用RSA公钥加密DES密钥。

1. 密码学的三大作用

  1. 加密:防止坏人获取你的数据
  2. 认证:防止坏人修改了你的数据而你却并没有发现
  3. 鉴权:防止坏人假冒你的身份

2. 数据摘要

一个数据源进行一个算法之后得到一个摘要,也叫作数据指纹。著名的摘要算法有RSA公司的MD5算法和SHA-1算法及其大量的变体。

消息摘要的主要特点

  • 无论输入的消息有多长,计算出来的消息摘要的长度总是固定的。例如应用MD5算法摘要的消息有128个比特位,用SHA-1算法摘要的消息最终有160比特位的输出。
  • 一般来说(不考虑碰撞的情况下),只要输入的原始数据不同,对其进行摘要以后产生的消息摘要也必不相同,即使原始数据稍有改变,输出的消息摘要便完全不同。但是,相同的输入必会产生相同的输出。
  • 具有不可逆性,即只能进行正向的信息摘要,而无法从摘要中恢复出任何的原始消息。

下面是Java中采用MD5和SHA-1进行摘要计算的代码:

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
package com.other;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

public class TestDigitalSummary {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "curriculum";

md5(null);
md5(s1);
md5(s2);

sha1(null);
sha1(s1);
sha1(s2);
}

private static void md5(String plainText) {
if (plainText == null || plainText.length() == 0) {
return;
}

try {
MessageDigest md5 = MessageDigest.getInstance("md5");
md5.reset();
md5.update(plainText.getBytes("UTF-8"));
byte[] digestByteArray = md5.digest();
System.out.println("the plain text is : " + plainText);
System.out.println("the length is : " + digestByteArray.length);//16 bytes represent 128 bits
System.out.println("the plain byte array is : " + Arrays.toString(digestByteArray));
System.out.println("the digital summary after md5 is : " + byteArrayTo32Md5String(digestByteArray));
System.out.println();
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
e.printStackTrace();
}
}

private static String byteArrayTo32Md5String(byte[] byteArray) {
if (byteArray == null || byteArray.length == 0) {
return "";
}

BigInteger bigInteger = new BigInteger(1, byteArray);
StringBuilder s = new StringBuilder(bigInteger.toString(16));
while (s.length() < 32) {
s.append("0").append(s);
}
return s.toString();
}

private static void sha1(String plainText) {
if (plainText == null || plainText.length() == 0) {
return;
}

try {
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
sha1.reset();
sha1.update(plainText.getBytes("UTF-8"));
byte[] digestByteArray = sha1.digest();
System.out.println("the plain text is : " + plainText);
System.out.println("the length is : " + digestByteArray.length);//20 bytes represent 128 bits
System.out.println("the plain byte array is : " + Arrays.toString(digestByteArray));
System.out.println("the digital summary after SHA-1 is : " + byteArrayToSha1String(digestByteArray));
System.out.println();
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
e.printStackTrace();
}
}
private static String byteArrayToSha1String(byte[] byteArray) {
if (byteArray == null || byteArray.length == 0) {
return "";
}

BigInteger bigInteger = new BigInteger(1, byteArray);
return bigInteger.toString(16);
}

}

其结果如下所示,可以看到md5之后得到了一个长度为32的字符串,而byte数组的长度是16,即128比特。而SHA-1之后得到了一个长度为40的字符串,而byte数组的长度为20,即160比特。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
the plain text is : abc
the length is : 16
the plain byte array is : [-112, 1, 80, -104, 60, -46, 79, -80, -42, -106, 63, 125, 40, -31, 127, 114]
the digital summary after md5 is : 900150983cd24fb0d6963f7d28e17f72

the plain text is : curriculum
the length is : 16
the plain byte array is : [-69, 99, 119, 60, -83, -105, -1, -49, -45, -97, 92, -4, 102, -19, 18, -26]
the digital summary after md5 is : bb63773cad97ffcfd39f5cfc66ed12e6

the plain text is : abc
the length is : 20
the plain byte array is : [-87, -103, 62, 54, 71, 6, -127, 106, -70, 62, 37, 113, 120, 80, -62, 108, -100, -48, -40, -99]
the digital summary after SHA-1 is : a9993e364706816aba3e25717850c26c9cd0d89d

the plain text is : curriculum
the length is : 20
the plain byte array is : [74, 19, 10, -20, 7, 4, -59, -94, -113, -70, 8, -111, 127, 3, 40, 21, -54, -35, -85, -14]
the digital summary after SHA-1 is : 4a130aec0704c5a28fba08917f032815caddabf2

3. 签名文件和证书

要确保通信安全,需要确保两个问题:① 要确定消息的来源确实是其申明的那个人,② 要保证信息在传递的过程中不被第三方篡改,即使被篡改了,也可以发觉出来

对于消息的发送者来说,先要生成一对公私钥对,将公钥给消息的接收者。

发送方:发送者发消息给接收者,对要发送的原始消息提取消息摘要,对提取的消息摘要用自己的私钥加密,这里就是原始信息的数字签名
接收方:对原始消息部分提取消息摘要,注意这里使用的消息摘要算法要和发送方使用的一致;对附加上的那段数字签名,使用预先得到的公钥解密;比较前两步所得到的两段消息是否一致。如果一致,则表明消息确实是期望的发送者发的,且内容没有被篡改过;相反,如果不一致,则表明传送的过程中一定出了问题,消息不可信。

通过这种所谓的数字签名技术,确实可以有效解决可靠通信的问题。如果原始消息在传送的过程中被篡改了,那么在接收者那里,对被篡改的消息提取的消息摘要肯定和原始的不一样。并且,由于篡改者没有消息发送方的私钥,即使他可以重新算出被篡改消息的摘要,也不能伪造出数字签名

数字签名和签名验证的大体流程

数字签名和签名验证的大体流程

2017年第一篇日志:【性能优化】知识汇总新鲜出炉,包括UI和组件的优化、图片的优化、线程优化、内存优化以及响应速度及卡顿优化,知道内存泄漏和内存溢出的区别吗?知道图片放错drawable文件夹导致失真的问题吗?赶紧瞅一瞅吧。

1. UI和组件的优化

1.1. Android中常见的度量单位

1.1.1. inch

它表示设备的物理屏幕的对角线长度,其中1 inch = 2.54 cm。

1.1.2. px

表示屏幕的像素。如分辨率为1920*1080,它表示屏幕的X方向上有1080个像素,Y方向上有1920个像素。

1.1.3. dpi和densityDpi

dot per inch简称为dpi,它表示每英寸上的像素点个数,所以它也常为屏幕密度。

在Android中使用DisplayMetrics中的densityDpi字段表示该值,并且不少文档中常用dpi来简化或者指代densityDpi。

如下屏幕密度对照表

屏幕密度对照表

问题:通过代码获取到的densityDpi和我们计算出来的屏幕实际密度值440.582不一样,为什么?

  • 在每部手机出厂时都会为该手机设置屏幕密度,若其屏幕的实际密度是440dpi那么就会将其屏幕密度设置为与之接近的480dpi;如果实际密度为325dpi那么就会将其屏幕密度设置为与之接近的20dpi。这也就是说常见的屏幕密度是与每个显示级别的最大值相对应的,比如:120、160、240、320、480、640等。

  • 有的手机不一定会选择120、160、240、320、480、640中的值作为屏幕密度,而是选择实际的dpi作为屏幕密度。比如小米手机,它的某些机型的densityDpi就是个非常规的值。

1.1.4. dp

density-independent pixel简称为dip或者dp,它表示与密度无关的像素。如果使用dp作为长度单位,那么该长度在不同密度的屏幕中显示的比例将保持一致。

如下密度无关像素与屏幕密度对照表

密度无关像素与屏幕密度对照表

1.1.5. sp

scale-independent pixel简称为sp,它类似于dp,但主要用于表示字体的大小。

1.2. 屏幕适配的问题

  1. 采用主流的分辨率设计UI(xhdpi或者xxhdpi)
  2. 放在正确的drawable文件夹中
  3. 统一使用dp和sp作为尺寸单位,利用dimens.xml自动切换
  4. 利用开源项目:SupportMultipleScreensUtil、com.android.support:percent:22.2.0
    • PercentRelativeLayout
    • PercentFrameLayout
    • 支持宽高以及margin

1.3. 布局优化

  1. 删除布局中无用的控件和层级
  2. 有选择的使用性能较低的ViewGroup:优先LinearLayout,FrameLayout
  3. 采用标签、标签和ViewStub。其中ViewStub的意义在于按需加载所需的布局文件

1.4. 绘制优化

  1. View的onDraw方法要避免执行大量的操作
  • onDraw中不要创建新的局部对象
  • onDraw方法中不要做耗时的任务,也不能执行成千上万次的循环操作。
  1. 尽管每次循环都很轻量级,但是大量的循环仍然十分抢占CPU的时间片,这样会造成View的绘制过程不流畅

1.5. ListView优化

1.5.1. 重用converView

通过复用converview来减少不必要的view的创建,另外Infalte操作会把xml文件实例化成相应的View实例,属于IO操作,是耗时操作。

1.5.2. 减少findViewById()操作

将xml文件中的元素封装成viewholder静态类,通过converview的setTag和getTag方法将view与相应的holder对象绑定在一起,避免不必要的findviewbyid操作。

1.5.3. 避免在 getView 方法中做耗时的操作

例如加载本地 Image 需要载入内存以及解析 Bitmap ,都是比较耗时的操作,如果用户快速滑动listview,会因为getview逻辑过于复杂耗时而造成滑动卡顿现象。用户滑动时候不要加载图片,待滑动完成再加载,可以使用这个第三方库glide

1.5.4. Item的布局层次结构尽量简单,避免布局太深或者不必要的重绘

1.5.5. 尽量能保证Adapter的hasStableIds() 返回 true

这样在 notifyDataSetChanged() 的时候,如果item内容并没有变化,ListView 将不会重新绘制这个 View,达到优化的目的。

在一些场景中,ScollView内会包含多个ListView,可以把listview的高度写死固定下来。由于ScollView在快速滑动过程中需要大量计算每一个listview的高度,阻塞了UI线程导致卡顿现象出现,如果我们每一个item的高度都是均匀的,可以通过计算把listview的高度确定下来,避免卡顿现象出现。

1.5.6. 使用 RecycleView 代替listview

每个item内容的变动,listview都需要去调用notifyDataSetChanged来更新全部的item,太浪费性能了。RecycleView可以实现当个item的局部刷新,并且引入了增加和删除的动态效果,在性能上和定制上都有很大的改善。

1.5.7. ListView中元素避免半透明

半透明绘制需要大量乘法计算,在滑动时不停重绘会造成大量的计算,在比较差的机子上会比较卡。在设计上能不半透明就不不半透明。实在要弄就把在滑动的时候把半透明设置成不透明,滑动完再重新设置成半透明。根据列表的滑动状态来控制任务的执行频率,比如当列表滑动时显然不太适合开启大量的异步任务。

1.5.8. 尽量开启硬件加速

硬件加速提升巨大,避免使用一些不支持的函数导致含泪关闭某个地方的硬件加速。当然这一条不只是对 ListView。

2. 图片的优化

2.1. Bitmap优化

  1. 及时的销毁
  • recycle并不能确定立即就会将Bitmap释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”,还有就是, 虽然recycle()从源码上看,调用它应该能立即释放Bitmap的主要内存,但是测试表明它并没能立即释放内存。故我们还需手动设置为NULL这样还能大大的加速Bitmap的主要内存的释放。
  1. 设置一定的采样率
  2. 使用缓存LruCache 以及本地缓存

2.2. drawable图片的加载导致失真的问题

图片的宽为144,高为180放在不同drawable中内存的消耗情况

假设手机的dpi值为480,把有一张图片放到drawable-xxhdpi里在手机上显示出来是不失真的,非常合适;但是错放到了drawable-xhdpi(其TypedValue的value值为320)后再次显示时发现图片被放大了,而且放大了480/320=1.5倍。既然图片被放大了那么该图片所占的内存当然也变大了。

这也就解释了我们有时遇到的类似困惑:为什么图片放在drawable-xxhdpi是正常的,但是放到drawable-mdpi后图片不仅仅放大失真而且所占内存也大幅增加了。

图片放在drawable-nodpi中,那么该图片不会被缩放;也就是说该图片在不同分辨率的手机上都只显示原图的大小。例如,把刚才这张图片放到drawable-nodpi中,那么它在各个手机上显示时它的宽均为144,高均为180。

3. 线程优化

3.1. 采用线程池

线程池可以重用内部的线程,从而避免了线程的创建和销毁所带来的性能开销

同时线程池还能有效的控制线程池的最大并发数,避免大量的线程因互相抢占系统资源而导致阻塞现象的发生

4. 内存优化

4.1. 内存管理

4.1.1. 分配机制

Android采用弹性的分配方式,也就是刚开始并不会一下分配很多内存给每个进程,而是给每一个进程分配一个“够用”的量。这个量是根据每一个设备实际的物理内存大小来决定的。

当前的内存可能不够使用了,这时候Android又会为每个进程分配一些额外的内存大小。

Android系统的宗旨是最大限度的让更多的进程存活在内存中,因为这样的话,下一次用户再启动应用,不需要重新创建进程,只需要恢复已有的进程就可以了,减少了应用的启动时间,提高了用户体验。

4.1.2. 回收机制

Android对内存的使用方式是“尽最大限度的使用”,这一点继承了Linux的优点。Android会在内存中保存尽可能多的数据,即使有些进程不再使用了,但是它的数据还被存储在内存中,所以Android现在不推荐显式的“退出”应用。

当用户下次再启动应用的时候,只需要恢复当前进程就可以了,不需要重新创建进程,这样就可以减少应用的启动时间。

只有当Android系统发现内存不够使用,需要回收内存的时候,Android系统就会需要杀死其他进程,来回收足够的内存。所以Android会有限清理那些已经不再使用的进程,以保证最小的副作用。

Android杀死进程有两个参考条件

  1. 进程优先级

    • 前台进程
    • 可见进程
    • 服务进程
    • 后台进程
    • 存放于一个LRU缓存列表中,先杀死处于列表尾部的进程
    • 空进程
    • 正常情况下,为了平衡系统整体性能,Android不保存这些进程
  2. 回收收益

    • 当Android系统开始杀死LRU缓存中的进程时,系统会判断每个进程杀死后带来的回收收益。因为Android总是倾向于杀死一个能回收更多内存的进程,从而可以杀死更少的进程,来获取更多的内存。杀死的进程越少,对用户体验的影响就越小。

4.1.3. 官方推荐的App内存使用方式

  1. 当Service完成任务后,尽量停止它
  2. 在UI不可见的时候,释放掉一些只有UI使用的资源
  3. 在系统内存紧张的时候,尽可能多的释放掉一些非重要资源
  4. 检查自己最大可用的内存大小
  5. 避免滥用Bitmap导致的内存浪费
  6. 使用针对内存优化过的数据容器
  7. 意识到内存的过度消耗
    • 避免创建不必要的对象。
    • 在合适的生命周期中,合理的管理资源。
    • 在系统内存不足时,主动释放更多的资源。
  8. 抽象代码也会带来更多的内存消耗
  9. 避免使用依赖注入的框架
  10. 使用多进程
    • 把消耗内存过大的模块,或者需要长期在后台运行的模块,移入到单独的进程中运行。Android会为每一个进程单独分配内存,所以理论上App就可以使用到更多的内存。但是多进程是一把双刃剑,错误的使用,会带来其他很多的问题。

4.2. 内存泄漏

指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

4.2.1. Activity引起内存泄漏

在 Java 中,非静态匿名内部类会持有其外部类的隐式引用,如果你没有考虑过这一点,那么存储该引用会导致 Activity 被保留,而不是被垃圾回收机制回收。

如果你的内存泄漏发生在 Activity 中,那么你将损失大量的内存空间。

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
/**
* 示例向我们展示了在 Activity 的配置改变时(配置改变会导致其下的 Activity 实例被销
* 毁)存活。此外,Activity 的 context 也是内存泄漏的一部分,因为每一个线程都被初始
* 化为匿名内部类,使得每一个线程都持有一个外部 Activity 实例的隐式引用,使得
* Activity 不会被 Java 的垃圾回收机制回收。
*/
public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
exampleOne();
}

private void exampleOne() {
new Thread() {
@Override
public void run() {
while (true) {
SystemClock.sleep(1000);
}
}
}.start();
}
}

举例来说,在使用应用的时候,你执行了10次横屏/竖屏操作,每一次方向的改变都会执行下面的代码,每一次配置的改变都会使 Android 系统新建一个 Activity 并把改变前的 Activity 交给垃圾回收机制回收。但因为线程持有旧 Activity 的隐式引用,使该 Activity 没有被垃圾回收机制回收。这样的问题会导致每一个新建的 Activity 都将发生内存泄漏,与 Activity 相关的所有资源文件也不会被回收,其中的内存泄漏有多严重可想而知。(该线程类声明为私有的静态内部类就可以解决这个问题)

4.2.2. AsyncTask导致的内存泄漏

问题点

  • 持有外部context强引用
  • 持有外部UI组件强引用

解决办法

  • 对context采用WeakRefrence,在使用之前判断是否为空。
  • 在Activity生命周期结束前,去cancel AsyncTask,因为Activity都要销毁了,这个时候再跑线程,绘UI显然已经没什么意义了。

4.2.3. 线程引起内存泄漏

在Java中线程是垃圾回收机制的根源,也就是说,在运行系统中DVM虚拟机总会使硬件持有所有运行状态的进程的引用,结果导致处于运行状态的线程将永远不会被回收。

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
/**
* 除了我们需要实现销毁逻辑以保证线程不会发生内存泄漏,其他代码和示例2相同。在退出当前
* Activity 前使用 onDestroy() 方法结束你的运行中线程是个不错的选择
*/
public class MainActivity extends Activity {
private MyThread mThread;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
exampleThree();
}

private void exampleThree() {
mThread = new MyThread();
mThread.start();
}

/**
* 私有的静态内部类不会持有其外部类的引用,使得 Activity 实例不会在配置改变时发生内
* 存泄漏
*/
private static class MyThread extends Thread {
private boolean mRunning = false;

@Override
public void run() {
mRunning = true;
while (mRunning) {
SystemClock.sleep(1000);
}
}

public void close() {
mRunning = false;
}
}

@Override
protected void onDestroy() {
super.onDestroy();
mThread.close();
}
}

4.2.4. 结论

  1. 避免写出内存泄露的代码
    • 静态变量导致的内存泄露
    • 单例模式导致的内存泄露,单例模式的特点是其生命周期和Application保持一致,因此Activity对象无法被及时释放
    • 属性动画导致的内存泄露,在Activity的onDestroy中调用animator.cancel()来停止动画
    • 创建过多对象导致内存在短时间内快速被消耗掉了
  2. 通过TraceView、MAT等工具找出潜在的内存泄露
  3. 适当的使用WeakReference
  4. 尽可能使用静态内部类而不是非静态内部类
  5. 不要总想着 Java 的垃圾回收机制会帮你解决所有内存回收问题
    • 为你的后台线程实现销毁逻辑是你在使用线程时必须时刻铭记的细节,此外,你在设计销毁逻辑时要根据 Activity 的生命周期去设计,避免出现 Bug。
  6. 考虑你是否真的需要使用线程

4.3. 内存溢出

指程序在申请内存时,没有足够的内存空间供其使用,出现OOM。memory leak会最终会导致out of memory。比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出

解决办法

  • 减少每个对象占用的内存,比如压缩图片
  • 申请大内存

4.4. APK大小优化

不必过度优化你的代码,要优化你的选择。

4.4.1. 优点

  1. 减少APP下载和安装的时间
  2. 减少APP安装后占用的存储空间
  3. 理论上更少的字节码也意味着需要执行的指令更少,需要加载进内存的代码页发生缺页的情况也更少,这些显然对于资源密集型使用场景例如应用冷启动起到很好的性能优化作用。

4.4.2. 具体实施

  1. 压缩图片
    • 没有alpha通道的png图,可压缩成jpg减少体积;
    • 对于体积特别大(超过50k)的图片资源可以考虑有损压缩,jpg采用优图压缩,png尝试采用pngquant压缩,输出视觉判断是否可行;
    • 采用webp格式的图片
    • 采用 ImageOptim压缩图片
  2. 删除无用资源

5. 响应速度及卡顿优化

大多数手机的屏幕刷新频率是60hz,如果在1000/60=16.67ms内没有办法把这一帧的任务执行完毕,就会发生丢帧的现象。丢帧越多,用户感受到的卡顿情况就越严重。

渲染操作通常依赖于两个核心组件:CPU与GPU。CPU负责包括Measure,Layout,Record,Execute的计算操作,GPU负责Rasterization(栅格化)操作。CPU通常存在的问题的原因是存在非必需的视图组件,它不仅仅会带来重复的计算操作,而且还会占用额外的GPU资源。

针对原理来作出解释如何优化

  • 减少视图的层级结构
  • 移除Window默认的Background
  • 移除XML布局文件中非必需的Background
  • 按需显示占位背景图片
  • 优化自定义view的ondraw方法
  • listview滑动取消图片加载
  • listview采用viewholder

花了将近7、8个小时粗略整理了自己遇到的一些关于网络通信的问题,也算是对这一大块内容的一个交代。这节的内容非常多,主要包括如下几个主题:网络各层的结构、TCP和UDP、Http协议的理解和用法、Socket编程、以及一些扩展,包括访问一个网页的流程、ping的整个过程等。当然一些常见的问题:三次握手四次挥手以及Socket与Http、Socket与TCP/IP的纠缠在文档中都有说明。

1. 网络各层的结构

1.1. OSI模型

物理层、数据链路层、网络层、传输层、会话层、表示层、应用层

1.2. 层模型

1.2.1 物理层、数据链路层、网络层、运输层、 应用层

1.3. 各层协议与作用

物理层

  • RJ45、CLOCK、IEEE802.3 (中继器,集线器,网关)
  • 通过媒介传输比特,确定机械及电气规范(比特Bit)

数据链路

  • PPP、FR、HDLC、VLAN、MAC  (网桥,交换机)
  • 将比特组装成帧和点到点的传递(帧Frame)

网络层

  • IP、ICMP、ARP、RARP、OSPF、IPX、RIP、IGRP、 (路由器)
  • 负责数据包从源到宿的传递和网际互连(包PackeT)

传输层

  • TCP、UDP、SPX
  • 提供端到端的可靠报文传递和错误恢复(段Segment)

会话层

  • NFS、SQL、NETBIOS、RPC
  • 建立、管理和终止会话(会话协议数据单元SPDU)

表示层

  • JPEG、MPEG、ASII
  • 对数据进行翻译、加密和压缩(表示协议数据单元PPDU)

应用层

  • FTP、DNS、Telnet、SMTP、HTTP、WWW、NFS
  • 允许访问OSI环境的手段(应用协议数据单元APDU)

2. UDP协议

3. TCP协议

3.1. TCP/UDP的区别

  • TCP提供面向连接的、可靠的数据流传输,而UDP提供的是非面向连接的、不可靠的数据流传输。TCP需要建立连接,而UDP不需要建立连接(无连接传输)

  • TCP传输单位称为TCP报文段,UDP传输单位称为用户数据报。

  • 是否建立真实连接的特性,造成了双方可靠性的差距。

    • TCP属于可靠的传输协议:因为传输前双方建立好了连接,相当于买卖双方建立好了交易合同,传输中一般不会出现意外,直到连接终止;

    • UDP属于不可靠的传输协议:UDP的所谓连接相当于一种映射,UDP单方面的认为目标地址(端口)是可用的,从而进行收发数据,而实际上目标地址(端口)未必可用,所以传输数据不可靠

  • 由于TCP需要建立真实的连接,所以需要消耗服务器的负载要大于UDP

3.2. TCP报文结构

tcp报文结构

源端口、目标端口

  • 计算机上的进程要和其他进程通信是要通过计算机端口的,而一个计算机端口某个时刻只能被一个进程占用,所以通过指定源端口和目标端口,就可以知道是哪两个进程需要通信。源端口、目标端口是用16位表示的,可推算计算机的端口个数为2^16个。

序列号

  • 表示本报文段所发送数据的第一个字节的编号。在TCP连接中所传送的字节流的每一个字节都会按顺序编号。由于序列号由32位表示,所以每2^32个字节,就会出现序列号回绕,再次从 0 开始。

  • 那如何区分两个相同序列号的不同TCP报文段就是一个问题了,后面会有答案,暂时可以不管。

确认号

  • 表示接收方期望收到发送方下一个报文段的第一个字节数据的编号。也就是告诉发送发:我希望你(指发送方)下次发送的数据的第一个字节数据的编号是这个确认号。也就是告诉发送方:我希望你(指发送方)下次发送给我的TCP报文段的序列号字段的值是这个确认号。

TCP首部长度

  • 由于TCP首部包含一个长度可变的选项部分,所以需要这么一个值来指定这个TCP报文段到底有多长。或者可以这么理解:就是表示TCP报文段中数据部分在整个TCP报文段中的位置。该字段的单位是32位字,即:4个字节。

URG

  • 表示本报文段中发送的数据是否包含紧急数据。URG=1,表示有紧急数据。后面的紧急指针字段只有当URG=1时才有效。

ACK

  • 表示是否前面的确认号字段是否有效。ACK=1,表示有效。只有当ACK=1时,前面的确认号字段才有效。TCP规定,连接建立后,ACK必须为1。

PSH

  • 告诉对方收到该报文段后是否应该立即把数据推送给上层。如果为1,则表示对方应当立即把数据提交给上层,而不是缓存起来。

RST

  • 只有当RST=1时才有用。如果你收到一个RST=1的报文,说明你与主机的连接出现了严重错误(如主机崩溃),必须释放连接,然后再重新建立连接。或者说明你上次发送给主机的数据有问题,主机拒绝响应。

SYN

  • 在建立连接时使用,用来同步序号。当SYN=1,ACK=0时,表示这是一个请求建立连接的报文段;当SYN=1,ACK=1时,表示对方同意建立连接。SYN=1,说明这是一个请求建立连接或同意建立连接的报文。只有在前两次握手中SYN才置为1。

FIN

  • 标记数据是否发送完毕。如果FIN=1,就相当于告诉对方:“我的数据已经发送完毕,你可以释放连接了”

窗口大小

  • 表示现在运行对方发送的数据量。也就是告诉对方,从本报文段的确认号开始允许对方发送的数据量。

校验和

  • 提供额外的可靠性。具体如何校验,参考其他资料。

紧急指针

  • 标记紧急数据在数据字段中的位置。

选项部分

  • 其最大长度可根据TCP首部长度进行推算。TCP首部长度用4位表示,那么选项部分最长为:(2^4-1)*4-20=40字节。

3.3. 三次握手

  • 第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。

  • 第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

  • 第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

三次握手示意

  • SYN攻击

    • 在三次握手过程中,Server发送SYN-ACK之后,收到Client的ACK之前的TCP连接称为半连接(half-open connect),此时Server处于SYN_RCVD状态,当收到ACK后,Server转入ESTABLISHED状态。

    • SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server回复确认包,并等待Client的确认,由于源地址是不存在的,因此,Server需要不断重发直至超时,这些伪造的SYN包将产时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络堵塞甚至系统瘫痪。

    • SYN攻击时一种典型的DDOS攻击,检测SYN攻击的方式非常简单,即当Server上有大量半连接状态且源IP地址是随机的,则可以断定遭到SYN攻击了,使用如下命令可以让之现行:#netstat -nap | grep SYN_RECV

3.4. 四次挥手

  • 第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。

  • 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。

  • 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。

  • 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。

  • 由于TCP连接是全双工的,因此每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。

四次挥手示意

为什么建立连接是三次而关闭连接却要四次呢?

  • 这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。

3.5. 流量控制

如果发送方把数据发送得过快,接收方可能会来不及接收,这就会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。

设A向B发送数据。在连接建立时,B告诉了A:“我的接收窗口是 rwnd = 400 ”(这里的 rwnd 表示 receiver window) 。因此,发送方的发送窗口不能超过接收方给出的接收窗口的数值。请注意,TCP的窗口单位是字节,不
是报文段。TCP连接建立时的窗口协商过程在图中没有显示出来。再设每一个报文段为100字节长,而数据报文段序号的初始值设为1。大写ACK表示首部中的确认位ACK,小写ack表示确认字段的值ack。

流量控制

从图中可以看出,B进行了三次流量控制。第一次把窗口减少到 rwnd = 300 ,第二次又减到了 rwnd = 100 ,最后减到 rwnd = 0 ,即不允许发送方再发送数据了。这种使发送方暂停发送的状态将持续到主机B重新发出一个新的窗口值为止。B向A发送的三个报文段都设置了 ACK = 1 ,只有在ACK=1时确认号字段才有意义。

TCP为每一个连接设有一个持续计时器(persistence timer)。只要TCP连接的一方收到对方的零窗口通知,就启动持续计时器。若持续计时器设置的时间到期,就发送一个零窗口控测报文段(携1字节的数据),那么收到这个报文段的一方就重新设置持续计时器。

3.6. 拥塞控制

拥塞

  • 即对资源的需求超过了可用的资源。若网络中许多资源同时供应不足,网络的性能就要明显变坏,整个网络的吞吐量随之负荷的增大而下降。

拥塞控制

  • 防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提:网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机、路由器,以及与降低网络传输性能有关的所有因素。

流量控制

  • 指点对点通信量的控制,是端到端正的问题。流量控制所要做的就是抑制发送端发送数据的速率,以便使接收端来得及接收。

拥塞控制代价

  • 需要获得网络内部流量分布的信息。在实施拥塞控制之前,还需要在结点之间交换信息和各种命令,以便选择控制的策略和实施控制。这样就产生了额外的开销。拥塞控制还需要将一些资源分配给各个用户单独使用,使得网络资源不能更好地实现共享。

几种拥塞控制方法

  • 慢开始算法

    • 当主机开始发送数据时,如果立即所大量数据字节注入到网络,那么就有可能引起网络拥塞,因为现在并不清楚网络的负荷情况。因此,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是说,由小到大逐渐增大拥塞窗口数值。通常在刚刚开始发送报文段时,先把拥塞窗口 cwnd 设置为一个最大报文段MSS的数值。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值。用这样的方法逐步增大发送方的拥塞窗口 cwnd ,可以使分组注入到网络的速率更加合理。
  • 拥塞控制算法

    • 当TCP连接进行初始化时,把拥塞窗口cwnd置为1。前面已说过,为了便于理解,图中的窗口单位不使用字节而使用报文段的个数。慢开始门限的初始值设置为16个报文段,即 cwnd = 16 。

    • 在执行慢开始算法时,拥塞窗口 cwnd 的初始值为1。以后发送方每收到一个对新报文段的确认ACK,就把拥塞窗口值另1,然后开始下一轮的传输(图中横坐标为传输轮次)。因此拥塞窗口cwnd随着传输轮次按指数规律增长。当拥塞窗口cwnd 增长到慢开始门限值ssthresh时(即当cwnd=16时),就改为执行拥塞控制算法,拥塞窗口按线性规律增长。

    • 假定拥塞窗口的数值增长到24时,网络出现超时(这很可能就是网络发生拥塞了)。更新后的ssthresh值变为12(即变为出现超时时的拥塞窗口数值24的一半),拥塞窗口再重新设置为1,并执行慢开始算法。当cwnd=ssthresh=12时改为执行拥塞避免算法,拥塞窗口按线性规律增长,每经过一个往返时间增加一个MSS的大小。

    • 强调:“拥塞避免”并非指完全能够避免了拥塞。利用以上的措施要完全避免网络拥塞还是不可能的。“拥塞避免”是说在拥塞避免阶段将拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞。

拥塞控制算法示意

  • 快重传算法

    • 如果发送方设置的超时计时器时限已到但还没有收到确认,那么很可能是网络出现了拥塞,致使报文段在网络中的某处被丢弃。这时,TCP马上把拥塞窗口 cwnd 减小到1,并执行慢开始算法,同时把慢开始门限值ssthresh减半。这是不使用快重传的情况。快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时才进行捎带确认。
  • 快恢复算法

    • 当发送方连续收到三个重复确认,就执行“乘法减小”算法,把慢开始门限ssthresh减半。这是为了预防网络发生拥塞。请注意:接下去不执行慢开始算法。

    • 由于发送方现在认为网络很可能没有发生拥塞,因此与慢开始不同之处是现在不执行慢开始算法(即拥塞窗口cwnd现在不设置为1),而是把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。下图给出了快重传和快恢复的示意图,并标明了“TCP Reno版本”。区别:新的 TCP Reno 版本在快重传之后采用快恢复算法而不是采用慢开始算法。

快恢复算法示意

  • 发送方维持一个拥塞窗口 cwnd ( congestion window )的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。发送方让自己的发送窗口等于拥塞。

  • 发送方控制拥塞窗口的原则是:只要网络没有出现拥塞,拥塞窗口就再增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数。

4. Http协议

4.1. 报文结构

请求报文

请求报文

响应报文

响应报文

4.2. 状态说明

状态代码有三位数字组成,第一个数字定义了响应的类别,且有五种可能取值

  • 100~199:指示信息,表示请求已接收,继续处理
  • 200~299:请求成功,表示请求已被成功接收、理解、接受
  • 300~399:重定向,要完成请求必须进行更进一步的操作
  • 400~499:客户端错误,请求有语法错误或请求无法实现
  • 500~599:服务器端错误,服务器未能实现合法的请求

常见的状态码如下

  • 200 OK 客户端请求成功
  • 400 Bad Request 客户端请求有语法错误,不能被服务器所理解
  • 401 Unauthorized 请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
  • 403 Forbidden 服务器收到请求,但是拒绝提供服务
  • 500 Internal Server Error 服务器发生不可预期的错误
  • 503 Server Unavailable 服务器当前不能处理客户端的请求,一段时间后可能恢复正常

4.3. Request的几种类型

OPTIONS

  • 返回服务器针对特定资源所支持的HTTP请求方法。也可以利用向Web服务器发送’*’的请求来测试服务器的功能性。

HEAD

  • 向服务器索要与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。

GET

  • 向特定的资源发出请求。

POST

  • 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的创建和 / 或已有资源的修改。

PUT

  • 向指定资源位置上传其最新内容。

DELETE

  • 请求服务器删除Request-URI所标识的资源。

TRACE

  • 回显服务器收到的请求,主要用于测试或诊断。

4.4. Http1.1和Http1.0的区别

HTTP连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。

  • HTTP 1.0规定浏览器与服务器只保持短暂的连接,而HTTP 1.1 支持长连接。

    • 在HTTP 1.0中,客户端的每次请求都要求建立一次单独的连接,在处理完本次请求后,就自动释放连接。

    • 在HTTP 1.1中则可以在一次连接中处理多个请求,并且多个请求可以重叠进行,不需要等待一个请求结束后再发送下一个请求。

  • HTTP 1.1支持持久连接,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。HTTP1.1还允许客户端不用等待上一次请求结果返回,就可以发出下一次请求,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容,这样也显著地减少了整个下载过程所需要的时间。

  • HTTP 1.1还提供了与身份认证、状态管理和Cache缓存等机制相关的请求头和响应头。

  • HTTP 1.1中增加Host请求头字段后,WEB浏览器可以使用主机头名来明确表示要访问服务器上的哪个WEB站点,这才实现了在一台WEB服务器上可以在同一个IP地址和端口号上使用不同的主机名来创建多个虚拟WEB站点。

4.5. Http怎么处理长连接

http长连接即持久连接是http1.1版本的一个特性,即一个http连接建立完成一个请求-回应后,可以不需要立刻关闭,可以重复使用。http的长连接是可以发送多个请求而不用等待每个响应的。

4.6. HTTP缓存机制

缓存对于移动端是非常重要的存在。

优点

  • 减少请求次数,减小服务器压力.
  • 本地数据读取速度更快,让页面不会空白几百毫秒。
  • 在无网络的情况下提供数据。

具体实施

  • 缓存一般由服务器控制(通过某些方式可以本地控制缓存,比如向过滤器添加缓存控制信息)。

http缓存相关请求头

  • 客户端发起请求的时候要检查缓存。遵循下面步骤,注意服务器返回304意思是数据没有变动滚去读缓存信息。

浏览器缓存机制

5. Socket

5.1. 套接字的概念

套接字(socket)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。

应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。

5.2. 建立socket连接

建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket ,另一个运行于服务器端,称为ServerSocket 。套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。

服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。

客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。

连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 与服务端建立Socket连接
Socket client = new Socket(GlobalParams.HOST, GlobalParams.PORT);
Log.e(TAG, "开始发送Socket请求");

// 往服务端写数据
writer = new PrintWriter(client.getOutputStream());
writer.write(packageJSON());// 将封装好的 JSON传递到服务端
writer.flush();// 字节流需要flush

// 获取服务端传回的数据
if (client.isConnected()) {
Log.e(TAG, "socket已连接成功")
reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
String line = null;
while ((line = reader.readLine()) != null) {
Log.e(TAG, "接收到了服务端传来的数据:" + line + "----长度:" + line.length());
result.append(line);
}
}
IOUtils.close(reader, writer);
client.close();

5.3. 短线重连怎么实现

利用心跳检测客户端是否与服务端连接正常,如断开了就发起重连

5.4. 心跳机制又是怎样实现

介绍

  • 心跳信息是单方向的,只有终端发到应用服务器;

  • 心跳信息的周期比较长,比如旧版QQ的心跳周期为30s,新版QQ为180s,微信为300s,Google原生应用为1680s左右。

  • 另外,互联网应用的心跳包除了宣告终端在线外,还有一项重要的任务,就是提供终端的即时地址,方便应用服务器的寻址。

  • 有了互联网应用的心跳机制,应用服务器可以及时下发(Push)用户相关的信息,比如微信中的短消息、图片或者语音等。

  • 心跳包也会带来很多副作用,比如终端更为费电,还可能给移动通信网络带来信令风暴。

实现

  • 轮询:定时去server查询数据

  • 推送:使用XMPP长连接

移动无线网络的特点

  • 因IPv4的数量有限,运营商分配给手机终端的 IP 是运营商内网的 IP,手机要连接 Internet,就需要通过运营商的网关做一个网络地址转换(Network Address Translation,NAT)。简单的说运营商的网关需要维护一个外网 IP、端口到内网 IP、端口的对应关系,以确保内网的手机可以跟 Internet 的服务器通讯

  • 我们知道移动端要和Internet进行通信,必须通过运营商的网关,所以,为了不让NAT映射表失效,我们需要定时向Internet发送数据,因为只是为了不然NAT映射表失效,所以只需发送长度为0的数据即可。

6. 扩展

6.1. Socket、TCP/IP与HTTP

6.1.1. Socket连接与TCP/IP连接

创建Socket连接时,可以指定使用的传输层协议,Socket可以支持不同的传输层协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接。

Socket是对TCP/IP协议的封装和应用(程序员层面上)。也可以说,TPC/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。

我们在传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如果没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有很多,比如HTTP、FTP、TELNET等,也可以自己定义应用层协议。WEB使用HTTP协议作应用层协议,以封装HTTP文本信息,然后使用TCP/IP做传输层协议将它发到网络上。

实际上socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket我们才能使用TCP/IP协议。Socket跟TCP/IP协议没有必然的联系。Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以说,Socket的出现只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象,从而形成了我们知道的一些最基本的函数接口,比如create、listen、connect、accept、send、read和write等等。

实际上,传输层的TCP是基于网络层的IP协议的,而应用层的HTTP协议又是基于传输层的TCP协议的,而Socket本身不算是协议,就像上面所说,它只是提供了一个针对TCP或者UDP编程的接口。Socket是对端口通信开发的工具,它要更底层一些。

6.1.2. Socket连接与HTTP连接

由于通常情况下Socket连接就是TCP连接,因此Socket连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。但在实际网络应用中,客户端到服务器之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 Socket 连接断连,因此需要通过轮询告诉网络,该连接处于活跃状态。

而HTTP连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。

很多情况下,需要服务器端主动向客户端推送数据,保持客户端与服务器数据的实时与同步。此时若双方建立的是Socket连接,服务器就可以直接将数据传送给客户端;若双方建立的是HTTP连接,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端,因此,客户端定时向服务器端发送连接请求,不仅可以保持在线,同时也是在“询问”服务器是否有新的数据,如果有就将数据传给客户端。

有个比较形象的描述:HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

两个计算机之间的交流无非是两个端口之间的数据通信,具体的数据会以什么样的形式展现,是以不同的应用层协议来定义的:如HTTP,FTP…

6.2. Cookie与Session的作用与原理

具体来说cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案。同时我们也看到,由于在服务器端保持状态的方案在客户端也需要保存一个标识,所以session机制可能需要借助于cookie机制来达到保存标识的目的,但实际上还有其他选择。

会话cookie和持久cookie的区别

  • 如果不设置过期时间,则表示这个cookie生命周期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览会话期的cookie被称为会话cookie。会话cookie一般不保存在硬盘上而是保存在内存里。

  • 如果设置了过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie依然有效直到超过设定的过期时间。

  • 存储在硬盘上的cookie可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存的cookie,不同的浏览器有不同的处理方式。

6.2.2. Sessioin

session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。当程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否已包含了一个session标识 - 称为session id,如果已包含一个session id则说明以前已经为此客户端创建过session,服务器就按照session id把这个session检索出来使用(如果检索不到,可能会新建一个),如果客户端请求不包含session id,则为此客户端创建一个session并且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个session id将被在本次响应中返回给客户端保存。

保存这个session id的方式可以采用cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发挥给服务器。一般这个cookie的名字都是类似于SEEESIONID,而。比如weblogic对于web应用程序生成的cookie,JSESSIONID=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764,它的名字就是JSESSIONID。 由于cookie可以被人为的禁止,必须有其他机制以便在cookie被禁止时仍然能够把session id传递回服务器。经常被使用的一种技术叫做URL重写,就是把session id直接附加在URL路径的后面,附加方式也有两种,一种是作为URL路径的附加信息,表现形式为http://…../xxx;jsessionid=ByOK … 99zWpBng!-145788764。另一种是作为查询字符串附加在URL后面,表现形式为http://…../xxx?jsessionid=ByOK …99zWpBng!-145788764

另一种技术叫做表单隐藏字段。就是服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时能够把session id传递回服务器。这种技术现在已较少应用,笔者接触过的很古老的iPlanet6(SunONE应用服务器的前身)就使用了这种技术。 实际上这种技术可以简单的用对action应用URL重写来代替。这两种方式对于用户来说是没有区别的,只是服务器在解析的时候处理的方式不同,采用第一种方式也有利于把session id的信息和正常程序参数区分开来。 为了在整个交互过程中始终保持状态,就必须在每个客户端可能请求的路径后面都包含这个session id。

在谈论session机制的时候,常常听到这样一种误解“只要关闭浏览器,session就消失了”。其实可以想象一下会员卡的例子,除非顾客主动对店家提出销卡,否则店家绝对不会轻易删除顾客的资料。对session来说也是一样的,除非程序通知服务器删除一个session,否则服务器会一直保留,程序一般都是在用户做log off的时候发个指令去删除session。然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分session机制都使用会话cookie来保存session id,而关闭浏览器后这个session id就消失了,再次连接服务器时也就无法找到原来的session。如果服务器设置的cookie被保存到硬盘上,或者使用某种手段改写浏览器发出的HTTP请求头,把原来的session id发送给服务器,则再次打开浏览器仍然能够找到原来的session。

恰恰是由于关闭浏览器不会导致session被删除,迫使服务器为seesion设置了一个失效时间,当距离客户端上一次使用session的时间超过这个失效时间时,服务器就可以认为客户端已经停止了活动,才会把session删除以节省存储空间。

6.3. 访问一个网页其整个过程是怎样的?

浏览器先尝试从Host文件中获取 http://www.baidu.com/ 对应的IP地址,如果能取到当然万事大吉大家都能嗨,如果不能,就使用DNS协议来获取IP咯。

得到百度的IP,下一步是使用TCP协议,建立TCP连接。(在TCP协议中,建立TCP需要与百度服务器握手三次,你先告诉服务器你要给服务器发东西(SYN),服务器应答你并告诉你它也要给你发东西(SYN、ACK),然后你应答服务器(ACK),总共来回了3次,称为3次握手。)

不过,建立TCP连接有个前提,为了将消息从你的PC上传到服务器上,需要用到IP协议、ARP协议和OSPF协议。你的PC和百度服务器之间一般会有许多路由器之类的东西,IP协议指定了出发地(你的PC)和目的地(服务器);你的数据会经过一个又一个路由器,OSPF决定了会经过那些路由器(用一种叫路由算法的玩意,找出最佳路径);从一个路由器怎么传给下一个路由器?这是ARP协议的JOB,ARP负责求下一个节点的地址(我们不止是要目的地,还要中间节点的地址)。IP协议使用的是IP地址,整个发送过程中只涉及出发地和目的地2个IP地址,而ARP协议使用的是MAC地址,整个发送过程中涉及到每一个节点的MAP地址

发送HTTP请求报文给服务器,如果服务器禁止你访问它就给你回个”Forbidden”,如果它暂时挂掉了就给你回个“内部服务错误”,如果它正常才给你回个“OK“并将你要的数据传给你;如果你还需要其它的东西再去跟它要(它一般还会给你的-_-)。

你收到了服务器的回复,是一坨HTML形式的文本。浏览器必须要能够理解文本的内容,并快速地渲染到屏幕上(浏览器一般用有限自动机来理解文本内容,渲染的话就各看本事了,之所以微软IE卡成狗而谷歌浏览器很6,就是它们的渲染速度不同…)

6.4. Ping的整个过程,ICMP报文是什么

6.4.1. ICMP报文

  • 在IP通信中,经常有数据包到达不了对方的情况。原因是,在通信途中的某处的一个路由器由于不能处理所有的数据包,就将数据包一个一个丢弃了。或者,虽然到达了对方,但是由于搞错了端口号,服务器软件可能不能接受它。这时,在错误发生的现场,为了联络而飞过来的信鸽就是ICMP 报文。在IP 网络上,由于数据包被丢弃等原因,为了控制将必要的信息传递给发信方。ICMP 协议是为了辅助IP 协议,交换各种各样的控制信息而被制造出来的。

icmp报文格式

  • 差错通知和信息查询

差错通知和信息查询

6.4.2. ping 命令

ping 命令用来在IP 层次上调查与指定机器是否连通,调查数据包往复需要多少时间。为了实现这个功能,ping 命令使用了两个ICMP 报文。

ping命令

同一网段内(实际过程的发生不到1毫秒)

  • 首先,如果主机A,要去ping主机B,那么主机A,就要封装二层报文,他会先查自己的MAC地址表,如果没有B的MAC地址,就会向外发送一个ARP广播包

  • 交换机会收到这个报文后,交换机有学习MAC地址的功能,所以他会检索自己有没有保存主机B的MAC地址,如果有,就返回给主机A,如果没有,就会向所有端口发送ARP广播,其它主机收到后,发现不是在找自己,就纷纷丢弃了该报文,不去理会。直到主机B收到了报文后,就立即响应,我的MAC地址是多少,同时学到主机A的MAC地址,并按同样的ARP报文格式返回给主机A。

跨网段的ping

  • 如果主机A要ping主机C,那么主机A发现主机C的IP和自己不是同一网段,他就去找网关转发,但是他也不知道网关的MAC地址情况下呢?他就会像之前那个步骤一样先发送一个ARP广播,学到网关的MAC地址,再发封装ICMP报文给网关路由器.。

  • 当路由器收到主机A发过来的ICMP报文,发现自己的目的地址是其本身MAC地址,根据目的的IP2.1.1.1,查路由表,发现2.1.1.1/24的路由表项,得到一个出口指针,去掉原来的MAC头部,加上自己的MAC地址向主机C转发。(如果网关也没有主机C的MAC地址,还是要向前面一个步骤一样,ARP广播一下即可相互学到。路由器2端口能学到主机D的MAC地址,主机D也能学到路由器2端口的MAC地址。)

  • 最后,在主机C已学到路由器2端口MAC地址,路由器2端口转发给路由器1端口,路由1端口学到主机A的MAC地址的情况下,他们就不需要再做ARP解析,就将ICMP的回显请求回复过来。

6.5. SSID、ESSID、BSSID区别

6.5.1. SSID

SSID是Service Set Identifier的缩写,意思是:服务集标识。SSID技术可以将一个无线局域网分为几个需要不同身份验证的子网络,每一个子网络都需要独立的身份验证,只有通过身份验证的用户才可以进入相应的子网络,防止未被授权的用户进入本网络。

什么是SSID?SSID(Service Set Identifier)也可以写为ESSID,用来区分不同的网络,最多可以有32个字符,无线网卡设置了不同的SSID就可以进入不同网络,SSID通常由AP广播出来,通过XP自带的扫描功能可以相看当前区域内的SSID。出于安全考虑可以不广播SSID,此时用户就要手工设置SSID才能进入相应的网络。简单说,SSID就是一个局域网的名称,只有设置为名称相同SSID的值的电脑才能互相通信。

禁用SSID广播通俗地说,SSID便是你给自己的无线网络所取的名字。需要注意的是,同一生产商推出的无线路由器或AP都使用了相同的SSID,一旦那些企图非法连接的攻击者利用通用的初始化字符串来连接无线网络,就极易建立起一条非法的连接,从而给我们的无线网络带来威胁。因此,建议最好能够将 SSID命名为一些较有个性的名字。

无线路由器一般都会提供“允许SSID广播”功能。如果不想让自己的无线网络被别人通过SSID名称搜索到,那么最好“禁止SSID广播”。你的无线网络仍然可以使用,只是不会出现在其他人所搜索到的可用网络列表中。

小提示:通过禁止SSID广播设置后,无线网络的效率会受到一定的影响,但以此换取安全性的提高,笔者认为还是值得的。

测试结果:由于没有进行SSID广播,该无线网络被无线网卡忽略了,尤其是在使用Windows XP管理无线网络时,达到了“掩人耳目”的目的。

6.5.2. BSSID

BSS:一种特殊的Ad-hoc LAN的应用,称为Basic Service Set (BSS),一群计算机设定相同的BSS名称,即可自成一个group,而此BSS名称,即所谓BSSID。

6.5.3. ESSID

ESSID(也称为服务区别号)将被放置在到每个无线访问接入点中,它是无线客户端与无线访问接入点联系所必不可少的。利用特定存取点的ESSID来做存取的控制,是AP的一种安全保护机制,它强制每一个客户端都必须要有跟存取点相同的ESSID值。但是,如果你在无线网卡上设定其ESSID为“ANY”时,它就可以自动的搜寻在讯号范围内所有的存取点,并试图连上它。

对于任何一个可能存取UWA-11接入点的适配器来说,无线设备首先决定这个适配器是否属于该网络,或扩展服务集。无线设备判断适配器的32位字符的标识ESSID是否和它自己的相符。即使有另外一套UWA-11产品,也没有人能够加入到网络或学习到跳频序列和定时。ESSID编程写入无线设备,并且在一个安装者密码的控制下,而且只能通过和设备的直接连接才能修改。如果需要在一个网络上有分别的网段,比如财务部门和公司其他部门拥有不同的网段,那么你可以编写不同的SSID。如果你需要支持移动用户和扩大带宽而连接多个无线设备,那么它们的SSID必须设置成一致而跳频序列应该不一样。所有这些设置都受UWA-11安装者密码的控制。
由于有了32位字符的SSID和3位字符的跳频序列,你会发现对于那些试图经由局域网的无线网段进入局域网的人来讲,想推断出确切的SSID和跳频序列有多么困难。


参考:

  1. Android网络请求心路历程

1、整机测试,而不测试拨号键盘应用,忽略所有错误,次数100万次

adb shell monkey –ignore-crashes –ignore-timeouts –pkg-blaklist-file -v -v 1000000

2、测试计算器30万次,随机种子为100,随机延迟0-1秒,忽略所有错误

adb shell monkey -p com.android.calculator2 -s 100 –throttle 1000 –randomize-throttle –ignore-crashes –ignore-timeouts -v -v 30000

3、测试计算器,触摸事件30%,其他按键50%,错误停止,延时200

adb shell monkey -p com.android.calculator2 –throttle 200 –pct-touch 30 –pct-anyevent 50 -v -v 100000

4、对计算器进行旋转压力测试,事件延时2秒,10万次

adb shell monkey -p com.android.calculator2 –pct-rotation 100 –throttle 2000 100000

5、仅对整机的应用开启测试,事件延时5秒,10万次

adb shell monkey –pct-appswitch 100 –throttle 5000 100000

定义: 为另一个对象提供一个替身或占位符以访问这个对象。

后续补充更多的内容:

在实际开发过程中,代理类的实现比上述代码要复杂很多,代理模式根据其目的和实现方式不同可分为很多种类,其中常用的几种代理模式简要说明如下:
(1) 远程代理(Remote Proxy):为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又称为大使(Ambassador)。
(2) 虚拟代理(Virtual Proxy):如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。
(3) 保护代理(Protect Proxy):控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。
(4) 缓冲代理(Cache Proxy):为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。
(5) 智能引用代理(Smart Reference Proxy):当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等。
在这些常用的代理模式中,有些代理类的设计非常复杂,例如远程代理类,它封装了底层网络通信和对远程对象的调用,其实现较为复杂。

下面的内容转载自:http://blog.csdn.net/lovelion/article/details/17517213

代理模式是常用的结构型设计模式之一,它为对象的间接访问提供了一个解决方案,可以对对象的访问进行控制。
代理模式类型较多,其中远程代理、虚拟代理、保护代理等在软件开发中应用非常广泛。

代理模式(一):代理模式概述,代理模式结构与实现
代理模式(二):代理模式应用实例(收费商务信息查询系统)
代理模式(三):远程代理,虚拟代理,缓冲代理
代理模式(四):代理模式效果与适用场景

关于java的代理机制及分析,下面有两篇文章详细的介绍:
Java 动态代理机制分析及扩展,第 1 部分
Java 动态代理机制分析及扩展,第 2 部分

定义: 允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。

下面的内容转载自:http://blog.csdn.net/lovelion/article/details/17517213

状态模式将一个对象在不同状态下的不同行为封装在一个个状态类中,通过设置不同的状态对象可以让环境对象拥有不同的行为,而状态转换的细节对于客户端而言是透明的,方便了客户端的使用。
在实际开发中,状态模式具有较高的使用频率,在工作流和游戏开发中状态模式都得到了广泛的应用,例如公文状态的转换、游戏中角色的升级等。

处理对象的多种状态及其相互转换——状态模式(一):银行系统中的账户类设计
处理对象的多种状态及其相互转换——状态模式(二):状态模式概述
处理对象的多种状态及其相互转换——状态模式(三):账户类的状态模式解决方案
处理对象的多种状态及其相互转换——状态模式(四):共享状态的实现
处理对象的多种状态及其相互转换——状态模式(五):使用环境类实现状态转换
处理对象的多种状态及其相互转换——状态模式(六):状态模式总结

定义: 允许你将对象组合成树形结构来表现“整体/部分”层次结构。组合能让客户以一致对方法处理个别对象以及对象组合。

在这章节中作者引出了非常基本的设计原则:

1
1. 类应该只有一个改变的理由  

组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。
同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构。
如果不使用组合模式,客户端代码将过多地依赖于容器对象复杂的内部实现结构,容器对象内部实现结构的变化将引起客户代码的频繁变化,带来了代码维护复杂、可扩展性差等弊端。组合模式的引入将在一定程度上解决这些问题。

下面的内容转载自:http://blog.csdn.net/lovelion/article/details/17517213

组合模式使用面向对象的思想来实现树形结构的构建与处理,描述了如何将容器对象和叶子对象进行递归组合,实现简单,灵活性好。
由于在软件开发中存在大量的树形结构,因此组合模式是一种使用频率较高的结构型设计模式,Java SE中的AWT和Swing包的设计就基于组合模式,在这些界面包中为用户提供了大量的容器构件(如Container)和成员构件(如Checkbox、Button和TextComponent等)

树形结构的处理——组合模式(一):设计杀毒软件的框架结构
树形结构的处理——组合模式(二):组合模式概述
树形结构的处理——组合模式(三):杀毒软件的框架结构的组合模式解决方案
树形结构的处理——组合模式(四):透明组合模式与安全组合模式
树形结构的处理——组合模式(五):公司组织结构,组合模式总结

定义: 提供一种方法顺序访问一个聚合对象中的某个元素,而又不是暴露其内部的表示。

在这章节中作者引出了非常基本的设计原则:

1
1. 类应该只有一个改变的理由

下面的内容转载自:http://blog.csdn.net/lovelion/article/details/17517213

迭代器模式是一种使用频率非常高的设计模式,通过引入迭代器可以将数据的遍历功能从聚合对象中分离出来,聚合对象只负责存储数据,而遍历数据由迭代器来完成。
由于很多编程语言的类库都已经实现了迭代器模式,因此在实际开发中,我们只需要直接使用Java、C#等语言已定义好的迭代器即可,迭代器已经成为我们操作聚合对象的基本工具之一。

遍历聚合对象中的元素——迭代器模式(一):销售管理系统中数据的遍历
遍历聚合对象中的元素——迭代器模式(二):迭代器模式概述
遍历聚合对象中的元素——迭代器模式(三):销售管理系统中数据的遍历的迭代器模式解决方案
遍历聚合对象中的元素——迭代器模式(四):使用内部类实现迭代器
遍历聚合对象中的元素——迭代器模式(五):JDK内置迭代器的使用
遍历聚合对象中的元素——迭代器模式(六):迭代器模式总结