Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

PNG,JPEG,BMP,JIF图片格式详解及其对比

$
0
0

图片格式详解

不知道大家有没有注意过网页里,手机里,平板里的图片,事实上,图片格式多样,不同平台对不同格式的图片支持也不一样,所以需要根据不同场合,使用不同格式的图片。

一.PNG格式

便携式网络图形(Portable Network Graphics,PNG)是一种无损压缩的位图图形格式,支持索引、灰度、RGB三种颜色方案以及Alpha通道等特性。

PNG格式有8位、24位、32位三种形式,其中8位PNG支持两种不同的透明形式(索引透明和alpha透明),24位PNG不支持透明,32位PNG在24位基础上增加了8位透明通道,因此可展现256级透明程度。

PNG8和PNG24后面的数字则是代表这种PNG格式最多可以索引和存储的颜色值。”8″代表2的8次方也就是256色,而24则代表2的24次方大概有1600多万色。

格式 最高支持色彩通道 索引色编辑支持 透明支持
PNG8 256索引色 支持 支持设定特定索引色为透明色(布尔透明)
支持为索引色附加8位透明度(256阶alpha透明)
PNG24 约1600万色 不支持 不支持
PNG32 约1600万色 不支持 支持8位透明度(256阶alpha透明)

1.PNG的文件结构

对于一个PNG文件来说,其文件头总是由位固定的字节来描述的:

进制 编码
十六进制数 89 50 4E 47 0D 0A 1A 0A

其中第一个字节0x89超出了ASCII字符的范围,这是为了避免某些软件将PNG文件当做文本文件来处理。文件中剩余的部分由3个以上的PNG的数据块(Chunk)按照特定的顺序组成,因此,一个标准的PNG文件结构应该如下:

PNG文件标志 | PNG数据块 …… | PNG数据块

2.PNG数据块(Chunk)

PNG定义了两种类型的数据块,一种是称为关键数据块(critical chunk),这是标准的数据块,另一种叫做辅助数据块(ancillary chunks),这是可选的数据块。关键数据块定义了4个标准数据块,每个PNG文件都必须包含它们,PNG读写软件也都必须要支持这些数据块。虽然PNG文件规范没有要求PNG编译码器对可选数据块进行编码和译码,但规范提倡支持可选数据块。

下表就是PNG中数据块的类别,其中,关键数据块部分我们使用_前缀加以区分。

数据块符号 数据块名称 多数据块 可选否 位置限制
_IHDR 文件头数据块 第一块
cHRM 基色和白色点数据块 在PLTE和IDAT之前
gAMA 图像γ数据块 在PLTE和IDAT之前
sBIT 样本有效位数据块 在PLTE和IDAT之前
_PLTE 调色板数据块 在IDAT之前
bKGD 背景颜色数据块 在PLTE之后IDAT之前
hIST 图像直方图数据块 在PLTE之后IDAT之前
tRNS 图像透明数据块 在PLTE之后IDAT之前
oFFs (专用公共数据块) 在IDAT之前
pHYs 物理像素尺寸数据块 在IDAT之前
sCAL (专用公共数据块) 在IDAT之前
_IDAT 图像数据块 与其他IDAT连续
tIME 图像最后修改时间数据块 无限制
tEXt 文本信息数据块 无限制
zTXt 压缩文本数据块 无限制
fRAc (专用公共数据块) 无限制
gIFg (专用公共数据块) 无限制
gIFt (专用公共数据块) 无限制
gIFx (专用公共数据块) 无限制
_IEND 图像结束数据 最后一个数据块

为了简单起见,我们假设在我们使用的PNG文件中,这4个数据块按以上先后顺序进行存储,并且都只出现一次。

(1)IHDR

文件头数据块IHDR(header chunk):它包含有PNG文件中存储的图像数据的基本信息,并要作为第一个数据块出现在PNG数据流中,而且一个PNG数据流中只能有一个文件头数据块。

文件头数据块由13字节组成,它的格式如下表所示。

域的名称 字节数 说明
Width 4 bytes 图像宽度,以像素为单位
Height 4 bytes 图像宽度,以像素为单位
Bit depth 1 bytes 颜色类型::
0:灰度图像, 1,2,4,8或16
2:真彩色图像,8或16
3:索引彩色图像,1,2,4或8
4:带α通道数据的灰度图像,8或16
6:带α通道数据的真彩色图像,8或16
ColorType 1 bytes 图像深度:
索引彩色图像:1,2,4或8
灰度图像:1,2,4,8或16
真彩色图像:8或16
Compression method 1 bytes 压缩方法(LZ77派生算法)
Filter method 1 bytes 滤波器方法
Interlace method 1 bytes 隔行扫描方法:
0:非隔行扫描
1:Adam7(由Adam M. Costello开发的7遍隔行扫描方法)

(2)PLTE

对于索引图像,调色板信息是必须的,调色板的颜色索引从0开始编号,然后是1、2……,调色板的颜色数不能超过色深中规定的颜色数(如图像色深为4的时候,调色板中的颜色数不可以超过2^4=16),否则,这将导致PNG图像不合法。

真彩色图像和带α通道数据的真彩色图像也可以有调色板数据块,目的是便于非真彩色显示程序用它来量化图像数据,从而显示该图像。

PLTE数据块是定义图像的调色板信息,PLTE可以包含1~256个调色板信息,每一个调色板信息由3个字节组成:

颜色 字节 说明
Red 1byte 0 = 黑色, 255 = 红
Green 1byte 0 = 黑色, 255 = 绿
Blue 1byte 0 = 黑色, 255 = 蓝

(3)IDAT

图像数据块IDAT(image data chunk):它存储实际的数据,在数据流中可包含多个连续顺序的图像数据块。

IDAT存放着图像真正的数据信息,因此,如果能够了解IDAT的结构,我们就可以很方便的生成PNG图像。

(4)IEND

图像结束数据IEND(image trailer chunk):它用来标记PNG文件或者数据流已经结束,并且必须要放在文件的尾部。

如果我们仔细观察PNG文件,我们会发现,文件的结尾12个字符看起来总应该是这样的:
00 00 00 00 49 45 4E 44 AE 42 60 82

不难明白,由于数据块结构的定义,IEND数据块的长度总是0(00 00 00 00,除非人为加入信息),数据标识总是IEND(49 45 4E 44),因此,CRC码也总是AE 42 60 82

3.数据块结构

PNG文件中,每个数据块由4个部分组成,如下:

名称 字节数 说明
Length (长度) 4字节 指定数据块中数据域的长度,其长度不超过(231-1)字节
Chunk Type Code (数据块类型码) 4字节 数据块类型码由ASCII字母(A-Z和a-z)组成
Chunk Data (数据块数据) 可变长度 存储按照Chunk Type Code指定的数据
CRC (循环冗余检测) 4字节 存储用来检测是否有错误的循环冗余码

CRC(cyclic redundancy check)域中的值是对Chunk Type Code域和Chunk Data域中的数据进行计算得到的。CRC具体算法定义在ISO 3309和ITU-T V.42中,其值按下面的CRC码生成多项式进行计算:

x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1

二.JPEG格式

其实JPEG 和JPG没有区别,JPG的全名、正式扩展名是JPEG。但因DOS、Windows 95等早期系统采用的8.3命名规则只支持最长3字符的扩展名,为了兼容采用了.jpg。也因历史习惯和兼容性考虑,.jpg目前更流行。

总的来说:JPEG是文件格式,JPG是扩展名。

JPG即使用JPEG文件交换格式存储的编码图像文件扩展名。 JPEG联合图象专家组,是一种压缩标准。两种文件应该没有区别,如果做文件扩展名严格地说应是JPG。

JPEG是一个压缩标准,又可分为标准 JPEG、渐进式JPEG及JPEG2000三种:

①标准JPEG:以24位颜色存储单个光栅图像,是与平台无关的格式,支持最高级别的压缩,不过,这种压缩是有损耗的。此类型图片在网页下载时只能由上而下依序显示图片,直到图片资料全部下载完毕,才能看到全貌。

②渐进式 JPEG:渐进式JPG为标准JPG的改良格式,支持交错,可以在网页下载时,先呈现出图片的粗略外观后,再慢慢地呈现出完整的内容,渐进式JPG的文件 比标准JPG的文件要来得小。

③JPEG2000:新一代的影像压缩法,压缩品质更好,其压缩率比标准JPEG高约30%左右,同时支持有损 和无损压缩。一个极其重要的特征在于它能实现渐进传输,即先传输图像的轮廓,然后逐步传输数据,让图像由朦胧到清晰显示。

JPEG(Joint Photographic Experts Group)是联合图像专家小组的英文缩写。它由国际电话与电报咨询委员会CCITT(The International Telegraph and Telephone Consultative Committee)与国际标准化组织ISO于1986年联合成立的一个小组,负责制定静态数字图像的编码标准。

JPEG专家组开发了两种基本的压缩算法、两种数据编码方法、四种编码模式。具体如下:
压缩算法:
(1)有损的离散余弦变换(Discrete Cosine Transform,DCT);
(2)无损的预测技术压缩。
数据编码方法:
(1)哈夫曼编码;
(2)算术编码;
编码模式:
(1)基于DCT顺序模式:编/解码通过一次扫描完成
(2)基于DCT递进模式:编/解码需要多次扫描完成,扫描效果从粗糙到精细,逐级递进
(3)无损模式:基于DPCM,保证解码后完全精确恢复到原图像采样值
(4)层次模式:图像在多个空间多种分辨率进行编码,可以根据需要只对低分辨率数据作解码,放弃高分辨率信息

1.JPEG文件结构介绍

JPEG文件使用的数据存储方式有多种,最常用的格式称为JPEG文件交换格式(JPEG File Interchange Format,JFIF)。而JPEG文件大体上可以分成两个部分:标记码(Tag)和压缩数据。

标记码由两个字节构成,其前一个字节是固定值0xFF,后一个字节则根据不同意义有不同数值。在每个标记码之前还可以添加数目不限的无意义的0xFF填充,也就说连续的多个0xFF可以被理解为一个0xFF,并表示一个标记码的开始。而在一个完整的两字节的标记码后,就是该标记码对应的压缩数据流,记录了关于文件的诸种信息。

常用的标记有SOI、APP0、DQT、SOF0、DHT、DRI、SOS、EOI。

注意,SOI等都是标记的名称。在文件中,标记码是以标记代码形式出现。例如SOI的标记代码为0xFFD8,即在JPEG文件中的如果出现数据0xFFD8,则表示此处为一个SOI标记。

下面附录列出完整的JPEG定义的标记码表:

(1)SOI,Start of Image,图像开始
标记代码|2字节|固定值0xFFD8

(2)APP0,Application,应用程序保留标记0
标记代码|2字节|固定值0xFFE0
包含9个具体字段:
① 数据长度|2字节|①~⑨9个字段的总长度,即不包括标记代码,但包括本字段
② 标识符|5字节|固定值0x4A46494600,即字符串“JFIF0”
③ 版本号|2字节|一般是0x0102,表示JFIF的版本号1.2,可能会有其他数值代表其他版本
④ X和Y的密度单位|1字节|只有三个值可选
0:无单位;
1:点数/英寸;
2:点数/厘米
⑤ X方向像素密度|2字节|取值范围未知
⑥ Y方向像素密度|2字节|取值范围未知
⑦ 缩略图水平像素数目|1字节|取值范围未知
⑧ 缩略图垂直像素数目|1字节|取值范围未知
⑨ 缩略图RGB位图|长度可能是3的倍数|缩略图RGB位图数据

本标记段可以包含图像的一个微缩版本,存为24位的RGB像素。如果没有微缩图像(这种情况更常见),则字段⑦“缩略图水平像素数目”和字段⑧“缩略图垂直像素数目”的值均为0。

(3)APPn,Application,应用程序保留标记n,其中n=1~15(任选)
标记代码|2字节|固定值0xFFE1~0xFFF
包含2个具体字段:
① 数据长度|2字节|①~②2个字段的总长度,即不包括标记代码,但包括本字段
② 详细信息|数据长度-2字节|内容不定

(4)DQT,Define Quantization Table,定义量化表
标记代码|2字节|固定值0xFFDB
包含9个具体字段:
① 数据长度|2字节|字段①和多个字段②的总长度,即不包括标记代码,但包括本字段
② 量化表|数据长度|2字节
a)精度及量化表ID|1字节|
高4位:精度,只有两个可选值
0:8位;
1:16位
低4位:
量化表ID,取值范围为0~3
b)表项|64×(精度+1))字节|例如8位精度的量化表,其表项长度为64×(0+1)=64字节

本标记段中,字段②可以重复出现,表示多个量化表,但最多只能出现4次。

(5)SOF0,Start of Frame,帧图像开始
标记代码|2字节|固定值0xFFC0
包含9个具体字段:
① 数据长度|2字节|①~⑥六个字段的总长度,即不包括标记代码,但包括本字段
② 精度|1字节|每个数据样本的位数,通常是8位,一般软件都不支持 12位和16位
③ 图像高度|2字节|图像高度(单位:像素),如果不支持 DNL 就必须 >0
④ 图像宽度|2字节|图像宽度(单位:像素),如果不支持 DNL 就必须 >0
⑤ 颜色分量数|1字节|只有3个数值可选
1:灰度图;
3:YCrCb或YIQ;
4:CMYK
而JFIF中使用YCrCb,故这里颜色分量数恒为3
⑥颜色分量信息|颜色分量数×3字节(通常为9字节)
a)颜色分量ID|1字节
b)水平/垂直采样因子|1字节|
高4位:水平采样因子
低4位:垂直采样因子
c)量化表|1字节|当前分量使用的量化表的ID

本标记段中,字段⑥应该重复出现,有多少个颜色分量(字段⑤),就出现多少次(一般为3次)。

(6)DHT,Difine Huffman Table,定义哈夫曼表
标记代码|2字节|固定值0xFFC4
包含2个具体字段:
①数据长度|2字节|字段①和多个字段②的总长度,即不包括标记代码,但包括本字段
② 哈夫曼表|数据长度-2字节
a)表ID和表类型|1字节|
高4位:类型,只有两个值可选
0:DC直流;
1:AC交流
低4位:哈夫曼表ID,注意,DC表和AC表分开编码
b)不同位数的码字数量|16字节
c)编码内容|16个不同位数的码字数量之和(字节)

本标记段中,字段②可以重复出现(一般4次),也可以致出现1次。例如,Adobe Photoshop 生成的JPEG图片文件中只有1个DHT标记段,里边包含了4个哈夫曼表;而Macromedia Fireworks生成的JPEG图片文件则有4个DHT标记段,每个DHT标记段只有一个哈夫曼表。

(7)DRI,Define Restart Interval,定义差分编码累计复位的间隔
标记代码|2字节|固定值0xFFDD
包含2个具体字段:
①数据长度|2字节|固定值0x0004,①~②两个字段的总长度, 即不包括标记代码,但包括本字段
②MCU块的单元中的重新开始间隔|2字节|设其值为n,则表示每n个MCU块就有一个,RSTn标记。第一个标记是RST0,第二个是RST1等,RST7后再从RST0重复。

如果没有本标记段,或间隔值为0时,就表示不存在重开始间隔和标记RST

(8)SOS,Start of Scan,扫描开始 12字节
标记代码|2字节|固定值0xFFDA
包含2个具体字段:
①数据长度|2字节|①~④两个字段的总长度,即不包括标记代码,但包括本字段
②颜色分量数|1字节|应该和SOF中的字段⑤的值相同,即:
1:灰度图是;
3: YCrCb或YIQ;
4:CMYK。
而JFIF中使用YCrCb,故这里颜色分量数恒为3
③颜色分量信息
a) 颜色分量ID|1字节
b) 直流/交流系数表号|1字节|
高4位:直流分量使用的哈夫曼树编号
低4位:交流分量使用的哈夫曼树编号
④ 压缩图像数据
a)谱选择开始|1字节|固定值0x00
b)谱选择结束|1字节|固定值0x3F
c)谱选择|1字节|在基本JPEG中总为00

本标记段中,字段③应该重复出现,有多少个颜色分量(字段②),就出现多少次(一般为3次)。本段结束后,紧接着就是真正的图像信息了。图像信息直至遇到一个标记代码就自动结束,一般就是以EOI标记表示结束。

(9)EOI,End of Image,图像结束 2字节
标记代码|2字节|固定值0xFFD9

这里补充说明一下,由于在JPEG文件中0xFF具有标志性的意思,所以在压缩数据流(真正的图像信息)中出现0xFF,就需要作特别处理。具体方法是,在数据0xFF后添加一个没有意义的0x00。换句话说,如果在图像数据流中遇到0xFF,应该检测其紧接着的字符,如果是
1)0x00,则表示0xFF是图像流的组成部分,需要进行译码;
2)0xD9,则与0xFF组成标记EOI,则图像流结束,同时图像文件结束;
3)0xD0~0xD7,则组成RSTn标记,则要忽视整个RSTn标记,即不对当前0xFF和紧接的0xDn两个字节进行译码,并按RST标记的规则调整译码变量;
3)0xFF,则忽视当前0xFF,对后一个0xFF再作判断;
4)其他数值,则忽视当前0xFF,并保留紧接的此数值用于译码。

2.JPEG图像编码

在实际应用中,JPEG图像编码算法使用的大多是离散余弦变换、Huffman编码、顺序编码模式。这样的方式,被人们称为JPEG的基本系统。这里介绍的JPEG编码算法的流程,也是针对基本系统而言。基本系统的JPEG压缩编码算法一共分为11个步骤:颜色模式转换、采样、分块、离散余弦变换(DCT)、Zigzag 扫描排序、量化、DC系数的差分脉冲调制编码、DC系数的中间格式计算、AC系数的游程长度编码、AC系数的中间格式计算、熵编码。

(1)颜色模式转换

JPEG采用的是YCrCb颜色空间,而BMP采用的是RGB颜色空间,要想对BMP图片进行压缩,首先需要进行颜色空间的转换。YCrCb颜色空间中,Y代表亮度,Cr,Cb则代表色度和饱和度(也有人将Cb,Cr两者统称为色度),三者通常以Y,U,V来表示,即用U代表Cb,用V代表Cr。RGB和YCrCb之间的转换关系如下所示:

Y = 0.299R+0.587G+0.114B

Cb = -0.1687R-0.3313G+0.5B+128

Cr = 0.5R=0.418G-0.0813B+128

一般来说,C 值 (包括 Cb Cr) 应该是一个有符号的数字, 但这里通过加上128,使其变为8位的无符号整数,从而方便数据的存储和计算。反之:

R = Y+1.402(Cr-128)

G = Y-0.34414(Cb-128)-0.71414(Cr-128)

B = Y+1.772(Cb-128)

(2)采样

研究发现,人眼对亮度变换的敏感度要比对色彩变换的敏感度高出很多。因此,我们可以认为Y分量要比Cb,Cr分量重要的多。在BMP图片中,RGB三个分量各采用一个字节进行采样,也就是我们常听到的RGB888的模式;而JPEG图片中,通常采用两种采样方式:YUV411和YUV422,它们所代表的意义是Y,Cb,Cr三个分量的数据取样比例一般是4:1:1或者4:2:2(4:1:1含义就是:在2x2的单元中,本应分别有4个Y,4个U,4个V值,用12个字节进行存储。经过4:1:1采样处理后,每个单元中的值分别有4个Y、1个U、1个V,只要用6个字节就可以存储了)。这样的采样方式,虽然损失了一定的精度但也在人眼不太察觉到的范围内减小了数据的存储量。当然,JPEG格式里面也允许将每个点的U,V值都记录下来。

(3)分块

由于后面的DCT变换是是对8x8的子块进行处理的,因此,在进行DCT变换之前必须把源图象数据进行分块。源图象中每点的3个分量是交替出现的,先要把这3个分量分开,存放到3张表中去。然后由左及右,由上到下依次读取8x8的子块,存放在长度为64的表中,即可以进行DCT变换。注意,编码时,程序从源数据中读取一个8x8的数据块后,进行DCT变换,量化,编码,然后再读取、处理下一个8*8的数据块。

JPEG 编码是以每8x8个点为一个单位进行处理的. 所以如果原始图片的长宽不是 8 的倍数, 都需要先补成8的倍数, 使其可以进行一块块的处理。将原始图像数据分为8*8的数据单元矩阵之后,还必须将每个数值减去128,然后一一带入DCT变换公式,即可达到DCT变换的目的。图像的数据值必须减去128,是因为DCT公式所接受的数字范围是-128到127之间。

(4)离散余弦变换

DCT(Discrete Cosine Transform,离散余弦变换),是码率压缩中常用的一种变换编码方法。任何连续的实对称函数的傅里叶变换中只含有余弦项,因此,余弦变换同傅里叶变换一样具有明确的物理意义。DCT是先将整体图像分成N*N的像素块,然后针对N*N的像素块逐一进行DCT操作。需要提醒的是,JPEG的编码过程需要进行正向离散余弦变换,而解码过程则需要反向离散余弦变换。

正向离散余弦变换计算公式:

F(u,v)=c(u)c(v)N1i=0N1j=0f(i,j)cos[(2i+1)π2Nu]cos[(2j+1)π2Nv]

c(u)=1N,u=0
c(u)=2N,u0

反向离散余弦变换计算公式:

f(i,j)=N1u=0N1v=0c(u)c(v)F(u,v)cos[(2i+1)π2Nu]cos[(2j+1)π2Nv]

c(u)=1N,u=0
c(u)=2N,u0

这里的N是水平、垂直方向的像素数目,一般取值为8。8 * 8的二维像素块经过DCT操作之后,就得到了8 * 8的变换系数矩阵。这些系数,都有具体的物理含义,例如,U=0,V=0时的F(0,0)是原来的64个数据的均值,相当于直流分量,也有人称之为DC系数或者直流系数。随着U,V的增加,相另外的63个系数则代表了水平空间频率和垂直空间频率分量(高频分量)的大小,多半是一些接近于0的正负浮点数,我们称之为交流系数AC。DCT变换后的8*8的系数矩阵中,低频分量集中在矩阵的左上角。高频成分则集中在右下角。

由于大多数图像的高频分量比较小,相应的图像高频分量的DCT系数经常接近于0,再加上高频分量中只包含了图像的细微的细节变化信息,而人眼对这种高频成分的失真不太敏感,所以,可以考虑将这一些高频成分予以抛弃,从而降低需要传输的数据量。这样一来,传送DCT变换系数的所需要的编码长度要远远小于传送图像像素的编码长度。到达接收端之后通过反离散余弦变换就可以得到原来的数据,虽然这么做存在一定的失真,但人眼是可接受的,而且对这种微小的变换是不敏感的。

(5)Zigzag扫描排序

DCT 将一个 8x8 的数组变换成另一个 8x8 的数组. 但是内存里所有数据都是线形存放的, 如果我们一行行的存放这 64 个数字, 每行的结尾的点和下行开始的点就没有什么关系, 所以 JPEG 规定按如下图中的数字顺序依次保存和读取64 个DCT的系数值。

这里写图片描述

这样数列里的相邻点在图片上也是相邻的了。不难发现,这种数据的扫描、保存、读取方式,是从8*8矩阵的左上角开始,按照英文字母Z的形状进行扫描的,一般将其称之为Zigzag扫描排序。如下图所示:

这里写图片描述

(6)量化

图像数据转换为DCT频率系数之后,还要进行量化阶段,才能进入编码过程。量化阶段需要两个8*8量化矩阵数据,一个是专门处理亮度的频率系数,另一个则是针对色度的频率系数,将频率系数除以量化矩阵的值之后取整,即完成了量化过程。当频率系数经过量化之后,将频率系数由浮点数转变为整数,这才便于执行最后的编码。不难发现,经过量化阶段之后,所有的数据只保留了整数近似值,也就再度损失了一些数据内容。在JPEG算法中,由于对亮度和色度的精度要求不同,分别对亮度和色度采用不同的量化表。前者细量化,后者粗量化。

下图给出JPEG的亮度量化表和色度量化表,该量化表是从广泛的实验中得出的。当然,你也可以自定义量化表。

JPEG亮度量化表
JPEG亮度量化表

JPEG色度量化表
JPEG色度量化表

这两张表依据心理视觉阀制作, 对 8bit 的亮度和色度的图象的处理效果不错。量化表是控制 JPEG 压缩比的关键,这个步骤除掉了一些高频量, 损失了很多细节信息。但事实上人眼对高频信号的敏感度远没有低频信号那么敏感。所以处理后的视觉损失很小,从上面的量化表也可以看出,低频部分采用了相对较短的量化步长,而高频部分则采用了相对较长的量化步长,这样做,也是为了在一定程度上得到相对清晰的图像和更高的压缩率。另一个重要原因是所有的图片的点与点之间会有一个色彩过渡的过程,而大量的图象信息被包含在低频率空间中,经过DCT处理后, 在高频率部分, 将出现大量连续的零。

(7)DC系数的差分脉冲调制编码

8*8的图像块经过DCT变换之后得到的DC系数有两个特点:

(1)系数的数值比较大;

(2)相邻的8*8图像块的DC系数值变化不大;

根据这两个特点,DC系数一般采用差分脉冲调制编码DPCM(Difference Pulse Code Modulation),即:取同一个图像分量中每个DC值与前一个DC值的差值来进行编码。对差值进行编码所需要的位数会比对原值进行编码所需要的位数少了很多。假设某一个8*8图像块的DC系数值为15,而上一个8*8图像块的DC系数为12,则两者之间的差值为3。

(8)DC系数的中间格式计算

JPEG中为了更进一步节约空间,并不直接保存数据的具体数值,而是将数据按照位数分为16组,保存在表里面。这也就是所谓的变长整数编码VLI。即,第0组中保存的编码位数为0,其编码所代表的数字为0;第1组中保存的编码位数为1,编码所代表的数字为-1或者1……,如下面的表格所示,这里,暂且称其为VLI编码表:

这里写图片描述

前面提到的那个DC差值为3的数据,通过查找VLI可以发现,整数3位于VLI表格的第2组,因此,可以写成(2)(3)的形式,该形式,称之为DC系数的中间格式。

(9)AC系数的行程长度编码(RLC)

量化之后的AC系数的特点是,63个系数中含有很多值为0的系数。因此,可以采用行程编码RLC(Run Length Coding)来更进一步降低数据的传输量。利用该编码方式,可以将一个字符串中重复出现的连续字符用两个字节来代替,其中,第一个字节代表重复的次数,第二个字节代表被重复的字符串。例如,(4,6)就代表字符串“6666”。但是,在JPEG编码中,RLC的含义就同其原有的意义略有不同。在JPEG编码中,假设RLC编码之后得到了一个(M,N)的数据对,其中M是两个非零AC系数之间连续的0的个数(即,行程长度),N是下一个非零的AC系数的值。采用这样的方式进行表示,是因为AC系数当中有大量的0,而采用Zigzag扫描也会使得AC系数中有很多连续的0的存在,如此一来,便非常适合于用RLC进行编码。

例如,现有一个字符串,如下所示:

57,45,0,0,0,0,23,0,-30,-8,0,0,1,000…..

经过RLC之后,将呈现出以下的形式:

(0,57) ; (0,45) ; (4,23) ; (1,-30) ; (0,-8) ; (2,1) ; (0,0)

注意,如果AC系数之间连续0的个数超过16,则用一个扩展字节(15,0)来表示16连续的0。

(10)AC系数的中间格式

根据前面提到的VLI表格,对于前面的字符串:

(0,57) ; (0,45) ; (4,23) ; (1,-30) ; (0,-8) ; (2,1) ; (0,0)

只处理每对数右边的那个数据,对其进行VLI编码: 查找上面的VLI编码表格,可以发现,57在第6组当中,因此,可以将其写成(0,6),57的形式,该形式,称之为AC系数的中间格式。

同样的(0,45)的中间格式为:(0,6),45;

(1,-30)的中间格式为:(1,5),-30;

(11)熵编码

在得到DC系数的中间格式和AC系数的中间格式之后,为进一步压缩图象数据,有必要对两者进行熵编码。JPEG标准具体规定了两种熵编码方式:Huffman编码和算术编码。JPEG基本系统规定采用Huffman编码(因为不存在专利问题),但JPEG标准并没有限制JPEG算法必须用Huffman编码方式或者算术编码方式。

Huffman编码:对出现概率大的字符分配字符长度较短的二进制编码,对出现概率小的字符分配字符长度较长的二进制编码,从而使得字符的平均编码长度最短。Huffman编码的原理请参考数据结构中的Huffman树或者最优二叉树。

Huffman编码时DC系数与AC系数分别采用不同的Huffman编码表,对于亮度和色度也采用不同的Huffman编码表。因此,需要4张Huffman编码表才能完成熵编码的工作。具体的Huffman编码采用查表的方式来高效地完成。然而,在JPEG标准中没有定义缺省的Huffman表,用户可以根据实际应用自由选择,也可以使用JPEG标准推荐的Huffman表。或者预先定义一个通用的Huffman表,也可以针对一副特定的图像,在压缩编码前通过搜集其统计特征来计算Huffman表的值。

下面我们举例来说明8*8图像子块经过DCT及量化之后的处理过程:

假设一个图像块经过量化以后得到以下的系数矩阵:

15 0 -1 0 0 0 0 0
-2 -1 0 0 0 0 0 0
-1 -1 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0

显然,DC系数为15,假设前一个8*8的图像块的DC系数量化值为12,则当前DC系统同上一个DC系数之间的差值为3,通过查找VLI编码表,可以得到DC系数的中间格式为(2)(3),这里的2代表后面的数字(3)的编码长度为2位;之后,通过Zigzag扫描之后,遇到第一个非0的AC系数为-2,遇到0的个数为1,AC系数经过RLC编码后可表示为(1,-2),通过查找VLI表发现,-2在第2组,因此,该AC系数的中间格式为(1,2)-2;
其余的点类似,可以求得这个8*8子块熵编码的中间格式为
(DC)(2)(3);AC(1,2)(-2),(0,1)(-1),(0,1)(-1),(0,1)(-1),(2,1)(-1),(EOB)(0,0)
对于DC系数的中间格式(2)(3)而言,数字2查DC亮度Huffman表得到011,数字3通过查找VLI编码表得到其被编码为11;
对于AC系数的中间格式(1,2)(-2)而言,(1,2)查AC亮度Huffman表得到11011,-2通过查找VLI编码表得到其被编码为01;

对于AC系数的中间格式(0,1)(-1)而言,(0,1)查AC亮度Huffman表得到00,数字-1通过查找VLI编码表得到其被编码为0;

对于AC系数的中间格式(2,1)(1)而言,(2,1)查AC亮度Huffman表得到11100,数字-1通过查找VLI编码表得到其被编码为0;

对于AC系数的中间格式(0,0)而言,查AC亮度Huffman表得到1010;
因此,最后这个8 * 8子块亮度信息压缩后的数据流为01111,1101101,000,000,000,111000,1010。总共31比特,其压缩比是64 * 8/31=16.5,大约每个像素用半个比特。

JPEG解码过程在此不再赘述,与编码过程对称。

三.BMP格式

BMP(Bitmap-File)图形文件是Windows采用的图形文件格式,在Windows环境下运行的所有图象处理软件都支持BMP图象文件格式。Windows系统内部各图像绘制操作都是以BMP为基础的。Windows 3.0以前的BMP图文件格式与显示设备有关,因此把这种BMP图象文件格式称为设备相关位图DDB(device-dependent bitmap)文件格式。Windows 3.0以后的BMP图象文件与显示设备无关,因此把这种BMP图象文件格式称为设备无关位图DIB(device-independent bitmap)格式(注:Windows 3.0以后,在系统中仍然存在DDB位图,象BitBlt()这种函数就是基于DDB位图的,只不过如果你想将图像以BMP格式保存到磁盘文件中时,微软极力推荐你以DIB格式保存),目的是为了让Windows能够在任何类型的显示设备上显示所存储的图象。BMP位图文件默认的文件扩展名是BMP或者bmp(有时它也会以.DIB或.RLE作扩展名)。

1.BMP格式结构

BMP文件的数据按照从文件头开始的先后顺序分为四个部分:

(1)位图文件头(bmp file header): 提供文件的格式、大小等信息
(2)位图信息头(bitmap information):提供图像数据的尺寸、位平面数、压缩方式、颜色索引等信息
(3)调色板(color palette):可选,如使用索引来表示图像,调色板就是索引与其对应的颜色的映射表
(4)位图数据(bitmap data):图像数据区

数据段名称 大小(byte) 开始地址 结束地址
位图文件头(bitmap-file header) 14 0000h 000Dh
位图信息头(bitmap-information header) 40 000Eh 0035h
调色板(color table) 由biBitCount决定 0036h 未知
图片点阵数据(bitmap data) 由图片大小和颜色定 未知 未知

2.BMP文件头

BMP文件头结构体定义如下:

typedef struct tagBITMAPFILEHEADER
{ 
UINT16 bfType;        //2Bytes,必须为"BM",即0x424D 才是Windows位图文件
DWORD bfSize;         //4Bytes,整个BMP文件的大小
UINT16 bfReserved1;  //2Bytes,保留,为0
UINT16 bfReserved2;  //2Bytes,保留,为0
DWORD bfOffBits;     //4Bytes,文件起始位置到图像像素数据的字节偏移量
} BITMAPFILEHEADER;
变量名 地址偏移 大小 作用说明
bfType 0000h 2Bytes 文件标识符,必须为”BM”,即0x424D才是Windows位图文件
bfSize 0002h 4Bytes 整个BMP文件的大小(以位B为单位)
bfReserved1 0006h 2Bytes 保留,必须设置为0
bfReserved2 0008h 2Bytes 保留,必须设置为0
bfOffBits 000Ah 4Bytes 说明从文件头0000h开始到图像像素数据的字节偏移量(以字节Bytes为单位),以为位图的调色板长度根据位图格式不同而变化,可以用这个偏移量快速从文件中读取图像数据

3.BMP信息头

BMP信息头结构体定义如下:

typedef struct _tagBMP_INFOHEADER
{
DWORD  biSize;    //4Bytes,INFOHEADER结构体大小,存在其他版本I NFOHEADER,用作区分
LONG   biWidth;    //4Bytes,图像宽度(以像素为单位)
LONG   biHeight;    //4Bytes,图像高度,+:图像存储顺序为Bottom2Top,-:Top2Bottom
WORD   biPlanes;    //2Bytes,图像数据平面,BMP存储RGB数据,因此总为1
WORD   biBitCount;         //2Bytes,图像像素位数
DWORD  biCompression;     //4Bytes,0:不压缩,1:RLE8,2:RLE4
DWORD  biSizeImage;       //4Bytes,4字节对齐的图像数据大小
LONG   biXPelsPerMeter;   //4 Bytes,用象素/米表示的水平分辨率
LONG   biYPelsPerMeter;   //4 Bytes,用象素/米表示的垂直分辨率
DWORD  biClrUsed;          //4 Bytes,实际使用的调色板索引数,0:使用所有的调色板索引
DWORD biClrImportant;     //4 Bytes,重要的调色板索引数,0:所有的调色板索引都重要
}BMP_INFOHEADER;

BMP信息头数据表如下:

变量名 地址偏移 大小 作用说明
biSize 000Eh 4Bytes BNP信息头即BMP_INFOHEADER结构体所需要的字节数(以字节为单位)
biWidth 0012h 4Bytes 说明图像的宽度(以像素为单位)
biHeight 0016h 4Bytes 说明图像的高度(以像素为单位)。这个值还有一个用处,指明图像是正向的位图还是倒向的位图,该值是正数说明图像是倒向的即图像存储是由下到上;该值是负数说明图像是倒向的即图像存储是由上到下。大多数BMP位图是倒向的位图,所以此值是正值。
biPlanes 001Ah 2Bytes 为目标设备说明位面数,其值总设置为1
biBitCount 001Ch 2Bytes 说明一个像素点占几位(以比特位/像素位单位),其值可为1,4,8,16,24或32
biCompression 001Eh 4Bytes 说明图像数据的压缩类型,取值范围为:
0 BI_RGB 不压缩(最常用)
1 BI_RLE8 8比特游程编码(BLE),只用于8位位图
2 BI_RLE4 4比特游程编码(BLE),只用于4位位图
3 BI_BITFIELDS比特域(BLE),只用于16/32位位图
biSizeImage 0022h 4Bytes 说明图像的大小,以字节为单位。当用BI_RGB格式时,总设置为0
biXPelsPerMeter 0026h 4Bytes 说明水平分辨率,用像素/米表示,有符号整数
biYPelsPerMeter 002Ah 4Bytes 说明垂直分辨率,用像素/米表示,有符号整数
biClrUsed 002Eh 4Bytes 说明位图实际使用的调色板索引数,0:使用所有的调色板索引
biClrImportant 0032h 4Bytes 说明对图像显示有重要影响的颜色索引的数目,如果是0,表示都重要。

4.BMP调色板

BMP调色板结构体定义如下:

typedef struct _tagRGBQUAD
{
BYTE  rgbBlue;       //指定蓝色强度
BYTE  rgbGreen;      //指定绿色强度
BYTE  rgbRed;        //指定红色强度
 BYTE  rgbReserved;  //保留,设置为0
} RGBQUAD;

1,4,8位图像才会使用调色板数据,16,24,32位图像不需要调色板数据,即调色板最多只需要256项(索引0 - 255)。
颜色表的大小根据所使用的颜色模式而定:2色图像为8字节;16色图像位64字节;256色图像为1024字节。其中,每4字节表示一种颜色,并以B(蓝色)、G(绿色)、R(红色)、alpha(32位位图的透明度值,一般不需要)。即首先4字节表示颜色号1的颜色,接下来表示颜色号2的颜色,依此类推。

颜色表中RGBQUAD结构数据的个数有biBitCount来确定,当biBitCount=1,4,8时,分别有2,16,256个表项。

当biBitCount=1时,为2色图像,BMP位图中有2个数据结构RGBQUAD,一个调色板占用4字节数据,所以2色图像的调色板长度为2*4为8字节。

当biBitCount=4时,为16色图像,BMP位图中有16个数据结构RGBQUAD,一个调色板占用4字节数据,所以16像的调色板长度为16*4为64字节。

当biBitCount=8时,为256色图像,BMP位图中有256个数据结构RGBQUAD,一个调色板占用4字节数据,所以256色图像的调色板长度为256*4为1024字节。

当biBitCount=16,24或32时,没有颜色表。

5.BMP图像数据区

位图数据记录了位图的每一个像素值,记录顺序是在扫描行内是从左到右,扫描行之间是从下到上。位图的一个像素值所占的字节数:
当biBitCount=1时,8个像素占1个字节;
当biBitCount=4时,2个像素占1个字节;
当biBitCount=8时,1个像素占1个字节;
当biBitCount=24时,1个像素占3个字节;
Windows规定一个扫描行所占的字节数必须是4的倍数(即以long为单位),不足的以0填充,
一个扫描行所占的字节数计算方法:
DataSizePerLine= (biWidth* biBitCount+31)/8;
// 一个扫描行所占的字节数
DataSizePerLine= DataSizePerLine/4*4; // 字节数必须是4的倍数
位图数据的大小(不压缩情况下):
DataSize= DataSizePerLine* biHeight;

颜色表接下来位为位图文件的图像数据区,在此部分记录着每点像素对应的颜色号,其记录方式也随颜色模式而定,既2色图像每点占1位(8位为1字节);16色图像每点占4位(半字节);256色图像每点占8位(1字节);真彩色图像每点占24位(3字节)。所以,整个数据区的大小也会随之变化。究其规律而言,可的出如下计算公式:图像数据信息大小=(图像宽度图像高度记录像素的位数)/8。

四.JIF格式

GIF是图像交换格式(Graphics Interchange Format)的简称,它是由美国CompuServe公司在1987年所提出的图像文件格式,它最初的目的是希望每个BBS的使用者能够通过GIF图像 文件轻易存储并交换图像数据,这也就是它为什么被称为图像交换格式的原因了。

 GIF文件格式采用了一种经过改进的LZW压缩算法,通常我们称 之为GIF-LZW算法。这是一种无损的压缩算法,压缩效率也比较高,并且GIF支持在一幅GIF文件中存放多幅彩色图像,并且可以按照一定的顺序和时间 间隔将多幅图像依次读出并显示在屏幕上,这样就可以形成一种简单的动画效果。尽管GIF最多只支持256色,但是由于它具有极佳的压缩效率并且可以做成动 画而早已被广泛接纳采用。下面笔者详细介绍GIF文件的格式。
 
 GIF图像文件是以块的形式来存储图像信息,其中的块又称为区域结构。按照其中 块的特征又可以将所有的块分成三大类,分别是控制块(Control Block)、图像描述块(Graphic Rendering Block)和特殊用途块(Special Purpose Block)。控制块包含了控制数据流的处理以及硬件参数的设置,其成员主要包括文件头信息、逻辑屏幕描述块、图像控制扩充块和文件结尾块。图像描述块包 含了在显示设备上描述图像所需的信息,其成员包括图像描述块、全局调色板、局部调色板、图像压缩数据和图像说明扩充块。特殊用途块包含了与图像数据处理无直接关系的信息,其成员包括图像注释扩充块和应用程序扩充块。下面详细介绍每一个块的详细结构。
 

1. 文件头信息

GIF的文件头只有六个字节,其结构定义如下:

typedef struct gifheader
{
      BYTE bySignature[3];
      BYTE byVersion[3];
}  GIFHEADER;

   其中,bySignature为GIF文件标示码,其固定值为“GIF”,使用者可以通过该域来判断一个图像文件是否是GIF图像格式的文件。 byVersion表明GIF文件的版本信息。其取值固定为“87a”和“89a”。分别表示GIF文件的版本为GIF87a或GIF89a。这两个版本 有一些不同,GIF87a公布的时间为1987年,该版本不支持动画和一些扩展属性。GIF89a是1989年确定的一个版本标准,只有89a版本才支持 动画、注释扩展和文本扩展。

2. 逻辑屏幕描述块

 逻辑屏幕(Logical Screen)是一个虚拟屏幕(Virtual Screen),它相当于画布,所有的操作都是在它的基础上进行的,同时它也决定了图像的长度和宽度。逻辑屏幕描述块共占有七个字节,其具体结构定义如下:

typedef struct gifscrdesc
{
      WORD wWidth;
      WORD wDepth;
      struct globalflag
         {
            BYTE PalBits   : 3;
            BYTE SortFlag  : 1;
            BYTE ColorRes  : 3;
            BYTE GlobalPal : 1;
         }  GlobalFlag;
      BYTE byBackground;
      BYTE byAspect;
}  GIFSCRDESC;

   其中,wWidth用来指定逻辑屏幕的宽度,wDepth用来指定逻辑屏幕的高度,glaobalflag为全域性数据,它的总长度为一个字节,其中前 三位(第0位到第2位)指定全局调色板的位数,可以通过该值来计算全局调色板的大小。第3位表明全局调色板中的RGB颜色值是否按照使用率进行从高到底的 次序排序的。第4到第6位指定图像的色彩分辨率。第7位指明GIF文件中是否具有全局调色板,其值取1表示有全局调色板,为0表示没有全局调色板。一个 GIF文件可以有全局调色板也可以没有全局调色板,如果定义了全局调色板并且没有定义某一幅图像的局部调色板,则本幅图像采用全局调色板;如果某一幅图像 定义的自己的局部调色板,则该幅图像使用自己的局部调色板。如果没有定义全局调色板,则GIF文件中的每一幅图像都必须定义自己的局部调色板。全局调色板 必须紧跟在逻辑屏幕描述块的后面,其大小由GlobalFlag.PalBits决定,其最大长度为768(3*256)字节。全局调色板的数据是按照 RGBRGB…..RGB的方式存储的。byBackground用来指定逻辑屏幕的背景颜色,也就相当于是画布的颜色。当图像长宽小于逻辑屏幕的大小 时,未被图像覆盖部分的颜色值由该值对应的全局调色板中的索引颜色值确定。如果没有全局调色板,该值无效,默认背景颜色为黑色。byAspect用来指定 逻辑屏幕的像素的长宽比例。

3. 图像描述块

  一幅GIF图像文件中可以存储多幅图像,并且这些图像没有固定的存放次序。为了区分两幅 图像,GIF采用了一个字节的识别码(Image Separator)来判断下面的数据是否是图像描述块。图像描述块以0x2C开始,定义紧接着它的图像的性质,包括图像相对于逻辑屏幕边界的偏移量、图 像大小以及有无局部调色板和调色板的大小。图像描述块由10个字节组成:

typedef struct gifimage
  {
        WORD wLeft;
        WORD wTop;
        WORD wWidth;
        WORD wDepth;
        struct localflag
           {
              BYTE PalBits   : 3;
              BYTE Reserved  : 2;
              BYTE SortFlag  : 1;
              BYTE Interlace : 1;
              BYTE LocalPal  : 1;
           }  LocalFlag;
   }  GIFIMAGE;

   其中,wLeft用来指定图像相对逻辑屏幕左上角的X坐标,以象素为单位。wTop用来指定图像相对逻辑屏幕左上角的Y坐标。wWdith和 wDepth分别用来指定图像的宽度和高度。LocalFlag用来指定区域性数据,也就是具体一幅图像的属性。LocalFlag的总长度为一个字节, 其中的前三位用来指定局部调色板的位数,可以根据该值来计算局部调色板的大小。第4位到第5位为保留位,没有使用,其值固定为0。第6位指明局部调色板中 的RGB颜色值是否经过排序,其值为1表示调色板中的RGB颜色值是按照其使用率从高到底的次序进行排序。第7位表示GIF图像是否以交错方式存储,其取 值为1表示以交错的方式进行存储。当图像是按照交错方式存储时,其图像数据的处理可以分为4个阶段:第一阶段从第0行开始,每次间隔8行进行处理;第二阶 段从第4行开始,每次间隔8行进行处理;第三阶段从第2行开始,每次间隔4行进行处理;第四阶段从第1行开始,每次间隔2行进行处理,这样当完成第一阶段 时就可以看到图像的概貌,当处理完第二阶段时,图像会变得清晰一些;当处理完第三阶段时,图像处理完成一半,清晰效果也进一步增强,当完成第四阶段,图像 处理完毕,显示出完整清晰的整幅图像。以交错方式存储是GIF文件格式的一个重要的特点,也是GIF文件格式的一个重要的优点。以交错方式存储的图像的好 处就是无需将整个图像文件解压完成就可以看到图像的概貌,这样可以减少用户的等待时间。第8位指明GIF图像是否含有局部调色板,如果含有局部调色板,则 局部调色板的内容应当紧跟在图像描述块的后面。

4. 图像压缩数据

 图像压缩数据是按照GIF-LZW压缩编码后存储于图像压缩数据块 中的。GIF-LZW编码是一种经过改良的LZW编码方式,它是一种无损压缩的编码方法。GIF-LZW编码方法是将原始数据中的重复字符串建立一个字符 串表,然后用该重复字符串在字符串表中的索引来替代原始数据以达到压缩的目的。由于GIF-LZW压缩编码的需要,必须首先存储GIF-LZW的最小编码 长度以供解码程序使用,然后再存储编码后的图像数据。编码后的图像数据是一个个数据子块的方式存储的,每个数据子块的最大长度为256字节。数据子块的第 一个字节指定该数据子块的长度,接下来的数据为数据子块的内容。如果某个数据子块的第一个字节数值为0,即该数据子块中没有包含任何有用数据,则该子块称 为块终结符,用来标识数据子块到此结束。
 

5. 图像控制扩充块

 图像控制扩充块是可选的,只应用于89a版本,它描述了与图像控制相关 的参数。一般情况下,图像控制扩充块位于一个图像块(包括图像标识符、局部颜色列表和图像数据)或文本扩展块的前面,用来控制跟在它后面的第一个图像(或 文本)的渲染(Render)形式,组成结构如下:
  

typedef struct gifcontrol
  {
        BYTE byBlockSize;
        struct flag
           {
              BYTE Transparency   : 1;
              BYTE UserInput      : 1;
              BYTE DisposalMethod : 3;
              BYTE Reserved       : 3;
           }  Flag;
        WORD wDelayTime;
        BYTE byTransparencyIndex;
        BYTE byTerminator;
  }  GIFCONTROL;

  其中,byBlockSize用来指定该图像控制扩充块的长度,其取值固定为4。Flag用来描述图像控制相关数据,它的长度为1个字节。它的第0位用 来指定图像中是否具有透明性的颜色,如果该位为1,这表明图像中某种颜色具有透明性,该颜色由参数byTransparencyIndex指定。第一位用 来判断在显示一幅图像后,是否需要用户输入后再进行下一个动作。如果该位为1,则表示应用程序在进行下一个动作之前需要用户输入。第2-4位用来指定图像 显示后的处理方式,当该值为0时,表示没有指定任何处理方式;当该值为1时,表明不进行任何处理动作;当该值为2时,表明图像显示后以背景色擦去;当该值 为3时,表明图像显示后恢复原先的背景图像。第5-7位为保留位,没有任何含义,固定为0。wDelayTime用来指定应用程序进行下一步操作之前延迟 的时间,单位为0.01秒。如果Flag.UserInput和wDelayTime都设定了,则以先发者为主,如果没有到指定的延迟时间即有用户输入, 则应用程序直接进行下一步操作。如果到达延迟时间后还没有用户输入,应用程序也直接进入下一步操作。byTransparenceIndex用来指定图像 中透明色的颜色索引,指定的透明色将不在显示设备上显示。byTerminator为块终结符,其值固定为0。

6. 图像说明扩充块

图像说明扩充块又可以称为图像文本扩展块,它用来绘制一个简单的文本图像,这一部分由用来绘制的纯文本数据(7位的 ASCII字符)和控制绘制的参数等组成。绘制文本借助于一个文本框(Text Grid)来定义边界,在文本框中划分多个单元格,每个字符占用一个单元,绘制时按从左到右、从上到下的顺序依次进行,直到最后一个字符或者占满整个文本 框(之后的字符将被忽略,因此定义文本框的大小时应该注意到是否可以容纳整个文本),绘制文本的颜色使用全局颜色列表,没有则可以使用一个已经保存的前一 个颜色列表。另外,图形文本扩展块也属于图形块(Graphic Rendering Block),可以在它前面定义图形控制扩展对它的表现形式进一步修改。图像说明扩充块的组成:

typedef struct gifplaintext
  {
        BYTE byBlockSize;
        WORD wTextGridLeft;
        WORD wTextGridTop;
        WORD wTextGridWidth;
        WORD wTextGridDepth;
        BYTE byCharCellWidth;
        BYTE byCharCellDepth;
        BYTE byForeColorIndex;
        BYTE byBackColorIndex;
  }  GIFPLAINTEXT;

  其中,byBlockSize用来指定该图像扩充块的长度,其取值固定为13。wTextGridLeft用来指定文字显示方格相对于逻辑屏幕左上角的 X坐标(以像素为单位)。wTextGridTop用来指定文字显示方格相对于逻辑屏幕左上角的Y坐标。wTextGridWidth用来指定文字显示方 格的宽度。wTextGridDepth用来指定文字显示方格的高度。byCharCellWidth用来指定字符的宽度, byCharCellDepth用来指定字符的高度。byForeColorIndex用来指定字符的前景色,byBackColorIndex用来指定 字符的背景色。
 

7. 图像注释扩充块

 图像注释扩充块包含了图像的文字注释说明,可以用来记录图形、版权、描述等任何的非图形和控制的 纯文本数据(7位的ASCII字符),注释扩展并不影响对图象数据流的处理,解码器完全可以忽略它。存放位置可以是数据流的任何地方,最好不要妨碍控制和 数据块,推荐放在数据流的开始或结尾。在GIF中用识别码0xFE来判断一个扩充块是否为图像注释扩充块。图像注释扩充块中的数据子块个数不限,必须通过 块终结符来判断该扩充块是否结束。

8. 应用程序扩充块

 应用程序扩充块包含了制作该GIF图像文件的应用程序的信息,GIF中用识别码0xFF来判断一个扩充块是否为应用程序扩充块。它的结构定义如下:

typedef struct gifapplication
  {
        BYTE byBlockSize;
        BYTE byIdentifier[8];
        BYTE byAuthentication[3];
  }  GIFAPPLICATION;

 其中,byBlockSize用来指定该应用程序扩充块的长度,其取值固定为12。byIdentifier用来指定应用程序名称。byAuthentication用来指定应用程序的识别码。
 

9. 文件结尾块

 文件结尾块为GIF图像文件的最后一个字节,其取值固定为0x3B。

总结

PNG格式图片分析

PNG这种图片格式包括了许多子类,但是在实践中大致可以分为256色的PNG(PNG8)和全色的PNG(PNG24、 PNG32),你完成可以用256色的PNG代替GIF,用全色的PNG代替JPEG

透明性:

PNG是完全支持alpha透明的(透明,半透明,不透明)

动画:

它不支持动画

无损耗性:

PNG是一种无损耗的图像格式,这也意味着你可以对PNG图片做任何操作也不会使 得图像质量产生损耗。这也使得PNG可以作为JPEG编辑的过渡格式
水平扫描像GIF一样,PNG也是水平扫描的,这样意味着水平重复颜色比垂直重复颜色的图片更小

间隔渐进显示:

它支持间隔渐进显示,但是会造成图片大小变得更大

优点:

  * 支持256色调色板技术以产生小体积文件
  * 最高支持48位真彩色图像以及16位灰度图像。
  * 支持Alpha通道的半透明特性。
  * 支持图像亮度的gamma校正信息。
  * 支持存储附加文本信息,以保留图像名称、作者、版权、创作时间、注释等信息。
  * 使用无损压缩。
  * 渐近显示和流式读写,适合在网络传输中快速显示预览效果后再展示全貌。
  * 使用CRC循环冗余编码防止文件出错。
  * 最新的PNG标准允许在一个文件内存储多幅图像。

缺点:

  但也有一些软件不能使用适合的预测,而造成过分臃肿的PNG文件。

JPEG格式图片特点

透明性、动画:

它并不支持透明,也不支持动画。

损耗性:

除了一些比如说旋转(仅仅是90、180、270度旋转),裁切,从标准类型到先进类型,编辑图片的原数据之外,所有其它操作对JPEG图像的处理 都会使得它的质量损失。所以我们在编辑过程一般用PNG作为过渡格式。

隔行渐进显示:

它支持隔行渐进显示(但是ie浏览器并不支持这个属性,但是ie会在整个图像信息完全到达的时候显示)。
由上可以看出JPEG是最适web上面的摄影图片和数字照相机中。

优点:

  JPEG/JFIF是最普遍在万维网(World Wide Web)上被用来储存和传输照片的格式。JPEG在色调及颜色平滑变化的相片或是写实绘画(painting)上可以达到它最佳的效果。在这种情况下,它通常比完全无失真方法作得更好,仍然可以产生非常好看的影像(事实上它会比其他一般的方法像是GIF产生更高品质的影像,因为GIF对于线条绘画(drawing)和图示的图形是无失真,但针对全彩影像则需要极困难的量化)。

缺点:

  它并不适合于线条绘图(drawing)和其他文字或图示(iconic)的图形,因为它的压缩方法用在这些图形的型态上,会得到不适当的结果;

GIF格式图片的特点

透明性:

Gif是一种布尔透明类型,既它可以是全透明,也可以是全不透明,但是它并没有半透明(alpha 透明)。

动画:

Gif这种格式支持动画。

无损耗性:

Gif是一种无损耗的图像格式,这也意味着你可以对gif图片做任何操作也不会使 得图像质量产生损耗。

水平扫描:

Gif是使用了一种叫作LZW的算法进行压缩的,当压缩gif的过程中,像素是由上到下水平压缩的,这也意味着同等条件下,横向的gif图片比竖向 的gif图片更加小。例如500*10的图片比10*500的图片更加小

间隔渐进显示:

Gif支持可选择性的间隔渐进显示

由以上特点看出只有256种颜色的gif图片不适合照片,但它适合对颜色要求不高的图形(比如说图标,图表等),它并不是最优的选择

优点:

  1. 优秀的压缩算法使其在一定程度上保证图像质量的同时将体积变得很小。
  2. 可插入多帧,从而实现动画效果。
  3. 可设置透明色以产生对象浮现于背景之上的效果。

缺点:

  由于采用了8位压缩,最多只能处理256种颜色(2的8次方),故不宜应用于真彩图像。

BMP格式图片的特点

透明性:

32位BMP有透明分量,可以通过添加通道使图片具有透明效果

动画:

不支持动画

无损耗性:

它采用位映射存储格式,除了图像深度可选以外,不采用其他任何压缩,因此,BMP文件所占用的空间很大

优点:

由于BMP文件格式是Windows环境中交换与图有关的数据的一种标准,因此在Windows环境中运行的图形图像软件都支持BMP图像格式。

BMP格式的图像,其优点是不采用任何压缩,无损,颜色准确,有2色、16色、256色、真彩色各种选择。

缺点:

缺点就是文件占用的空间很大,不支持文件压缩,不适用于 Web 页,不受 Web 浏览器支持。

作者:u012611878 发表于2016/8/25 0:19:52 原文链接
阅读:260 评论:0 查看评论

[React Native混合开发]React Native for iOS之应用

$
0
0

JSX在React-Native中的应用

一、JSX概述

你一定疑问为什么要用JSX?其实这不是必需,而是建议。只是因为React是作为MVC中的V,是为UI而生,所以,React-Native使用JSX更能像HTML样表达树形结构,其实HTML的超类就是XML,React-Native将这个带到了解放前,不可否认的是JSX相比节省了很多的代码。JSX不是什么新奇的东西,JSX只是对JavaScript进行了拓展,仅此而已。

二、语法介绍

1、类XML UI组件表达,在React-Native中表现为:
render: function() {
    return (
        <View style={styles.container}>
            <Text style={styles.welcome}>
                Welcome to React Native!
            </Text>
        </View>
    );
}

2、js表达式
在JSX中,表达式需要{}包裹,例如:
render: function() {
    return (
        <View style={styles.container}>
            <Text style={styles.welcome}>
                {0? '第一段': '第二段'}
            </Text>
        </View>
    );
}
上面的代码我们可以看出,style={}是一个表达式;{0? '第一段': '第二段'}是表达式,最后显示的应该是“第二段”。

3、属性
在HTML中,属性可以是任何值,例如:<div tagid="00_1"></div>,tagid就是属性;同样,在组件上可以使用属性。
建议使用以下方式:
var props = {
    tagid: 'GGFSJGFFATQ',
    poiname: '东方明珠'
};
return (<View {...props}></View>);

4、如果需要在调用组件的时候动态增加或者覆盖属性,又该如何呢?
很简单:<View {...props} poiname={'虹桥机场'}></View>

5、关于样式
(1)普通内联样式:{{}},第一层{}是表达式,第二层{}是js对象;
<View style={{fontSize:40, width:80,}}> </View>
(2)调用样式表:{样式类.属性}
<View style={styles.container}></View>
(3)样式表和内联样式共存:{[]}
<View style={[styles.container, {fontSize:40, width:80}]}>
(4)多个样式表:{[样式类1, 样式类2]}
<View style={[styles.container, styles.color]}>

6、属性校验
为了实现强类型语言的效果,我们可以使用propTypes来声明数据属性的合法性校验。例如:
React.createClass({
    porpTypes:{
        username: React.PropTypes.string,
        age: React.propTypes.number,
    }
});

7、设定默认属性
React.createClass({
    getDefaultProps: function(){
        return {
            sign: '这个家伙很懒,什么都没留下'
        };
    }
});

8、组件的生命周期
componentWillMount:组件创建之前
getInitialState:初始化状态
render:渲染视图
componentDidMount:渲染视图完成后
componentWillUnmount:组件被卸载之前

三、了解虚拟DOM

React进行了虚拟DOM的封装,所有的视图的更新都是虚拟DOM做了一个校验(diff)后最小更新。为什么这么做,因为现在机器的内存已经足以支撑这样视图UI的diff计算,用内存计算换取UI渲染效率。

1、我们需要获取组件中真实的dom
React.findDOMNode(component)

2、第二节已经简单说了组件的生命周期(will, did)
组件的生命周期分为3个部分:
Mounting:正在装载组件;
Updating:重新计算渲染组件;
Unmounting:卸载组件
作者:BaiHuaXiu123 发表于2016/8/25 0:52:50 原文链接
阅读:329 评论:1 查看评论

J2V8入门教程

$
0
0

开始使用J2V8

J2V8是一套针对谷歌的V8 Javascript引擎的java绑定。J2V8的开发为Android平台带来了高效的Javascript的执行环境,taris.js 就是基于J2V8开发的。J2V8同时也可以运行在Windows、Linux、MacOS上。在本教程中我们将展示如何使用J2V8来执行javascript脚本


尽可能的“原始”

J2V8以性能与内存消耗为设计目标,如果一段Javascript代码的执行结果是一个32位整数,那么它可以直接作为一个原始类型被访问( access,存取)而不用先为它创建一个包装类型的实例(译注:而在其他java javascript引擎,比如在Rhino中,不能直接在java与javascript间直接存取数据,而必须先把它包装成一个java对象)。这对于64位的浮点数(doubles)和布尔类型的数据来说同样是适用的。

J2V8 也使用“懒加载”技术。也就是说,只有当Javascript的执行结果被访问(被使用到)的时候,它(执行结果)才会通过JNI被拷贝到java中。举例来说,如果javascript返回了一个大型的数组,这个数组的内容直到数组中的元素的被需要的时候才会被加载到java中。


本地对象句柄

J2V8仅仅是一组将V8引擎的API暴露给Java的java本地接口(JNI)。而V8引擎本身是使用C++写成的。为了访问到V8引擎,J2V8就使用了JNI对其进行封装。因为要与V8本地库进行交互,因此由此产生的C++内存必须得到妥善的管理。J2V8为管理本地对象句柄提供了一些提供了一些帮助,但是仍然需要开发者在这些对象不再被使用时,明确地调用release()来释放这些本地对象句柄。释放一个对象并不会把它从javascript中给释放掉(这是V8引擎自带的垃圾回收器要干的事),释放仅仅是移除了本地对象句柄。释放资源的规则非常简单:
1. 如果是你自己创建的本地对象,那么你必须释放它,只有一个例外,那就是,如果一个对象是通过返回语句传回来的话,系统会替你释放它。
2. 如果一个对象是由系统创建的话,你无需担心它们,只有一种情况需要操心,那就是,如果对象作为一个方法的调用的结果返回的话,那你就必须手动的释放它。

为了帮助你管理本地对象句柄,J2V8可以被设置成在程序结束时报告任何的内存泄露(译注:即如果有任何应该被手动释放而没有被释放的对象,在程序结束时会在控制台打印出类似”xx Object(s) still exist in runtime”的信息,这同样也可能引发一个 “java.lang.IllegalStateException: 1 Object(s) still exist in runtime”异常)。


多线程模型

Javascript本身是单线程的,而J2V8则强化了这一点。使用对同一个运行时(runtime)的访问都必须是来自同一线程的,这一点确保了在对某个Javascript运行时进行控制和使用的时候不会有出现竞态条件或者死锁的潜在风险。

尽管对在J2V8中对同一个运行时而言只能由同一个线程进行访问,你仍然可以创建多个运行时,然后为每一个运行时都存在于它们自己的线程之中。在这种多线程的模型之下,你可以很容易的实现 WebWorkers


获取J2V8

J2V8可以通过Maven Centeral获得。目前最新的版本是2.2.1(译注:在译者翻译本文时J2V8版本已经迭代到了4.5,添加了不少新的特性,仍然可以通过Maven获得)。以下的代码示例可以在你的pom.xml添加对J2V8的依赖。

<dependency>
  <groupId>com.eclipsesource.j2v8</groupId>
  <artifactId>j2v8_win32_x86_64</artifactId>
  <version>2.2.1</version>
</dependency>

上面的代码将会从Maven Centeral的服务器上获取64位windows版的J2V8。
(译注:如果你想要实用较新的版本的话注意将version替换成相应的版本号)

其他特定平台的运行时环境包括:

  • j2v8_win32_x86
  • j2v8_android_x86
  • j2v8_android_armv7l
  • j2v8_macosx_x86_64

(译注:当然也包括linux平台下的j2v8_linux_x86_64可以获取)


Hello, World!

为了在实践中理解J2V8,让我们一起来创建一段Hello World,这段脚本将两个字符串连接起来并且返回了结果字符串的长度

 var hello = 'hello, ';
 var world = 'world!';
hello.concat(world).length();

(译注:经测试,上面那段脚本在最新的J2V8中已经不能正常运行,原因是length不再被实现为一个函数而是被实现为一个属性,所以应当将hello.concat(world).length();改为hello.concat(world).length; 以下所有代码以修改过的代码给出,如果对原来的代码感兴趣的请参考文末给出的原文链接。)

要使用J2V8,首先你必须创建一个运行时环境,J2V8为此提供了一个静态工厂方法。在创建一个运行时环境时,同时也会加载J2V8的本地库。

public static void main(String[] args) {
 V8 runtime = V8.createV8Runtime();
 int result = runtime.executeIntegerScript(""
  + "var hello = 'hello, ';\n"
  + "var world = 'world!';\n"
  + "hello.concat(world).length();\n");
 System.out.println(result);
 runtime.release();
}

一单运行时环境创建好了,你就可以在上面执行javascript脚本了。为了执行脚本,它提供了多个基于不同返回值的执行方法。在这个例子里,我们使用了executeIntegerScript() 这个方法,因为脚本执行的结果是一个int类型的整数,并且不需要任何的类型转换和包装。当应用结束时,运行时环境必须被释放。


在Java中访问Javascript对象

使用J2V8你可以从Java中获取javascript对象的句柄(译注:换言之你可以在java中获取到javascript中的对象,并且对其进行操作)。接下来的嵌在java中javascript脚本代码演示了这一点:

public static void main(String[] args) {
  V8 runtime = V8.createV8Runtime();
  runtime.executeVoidScript(""
    + "var person = {};\n"
    + "var hockeyTeam = {name : 'WolfPack'};\n"      
    + "person.first = 'Ian';\n"
    + "person['last'] = 'Bull';\n"
    + "person.hockeyTeam = hockeyTeam;\n");
  // TODO: Access the person object
  runtime.release();
}

在执行完上面的脚本代码之后,你就可以在java中轻易的通过名称访问(获取)到javascript中的哪些全局变量了。在这个例子中,我们可以获取到person 对象,并且以这个对象为起点,周游(walking,遍历)它的对象图(译注:object graph,个人理解就与这个对象直接或者间接关联的其他对象的集合)。

  V8Object person = runtime.getObject("person");
  V8Object hockeyTeam = person.getObject("hockeyTeam");
  System.out.println(hockeyTeam.getString("name"));
  person.release();
  hockeyTeam.release();

因为V8Object只是底层javascript对象的句柄(译注:句柄,可以理解为对象的引用,反正是可以通过它来对对象进行操作的东西),所以可以对V8Object对象进行操作。举个例子,我们可以在java中为javascript增加新的属性,比如hockeyTeam.add("captain", person); 在进行了这一步操作之后,新添加的属性captain 可以在javascript中立刻被访问到。以下代码可以验证这一点:

assertTrue(runtime.executeBooleanScript("person === hockeyTeam.captain"));

V8Object也提供了其他一些有用的方法。Object.getKeys() 会返回所有关联到对象中的键(译注:javascript对象实质上可以看成一组key-value的property集合)。而Object.getType(String key) 则会返回key所对应对象的类型。以上两种方法结合起来,你可以动态地周游( traverse,遍历)一个复杂的对象。

最后,我们访问(获取)过的V8Object对象在我们不再使用它们的时候必须被释放掉。如果javascript底层仍然处于可访问的状态(译注:即没有超出它们的javascript作用域),那么它们在javascript脚本中依然是存在的。它们之所以需要我们手动的释放,是因为它们是作为方法调用的结果返回给我们的(参见第二节)。


V8Arrays(V8数组)

正向V8Object可以从Java中被访问一样,V8数组也可以通过JNI中间层在javascript与java之间传递。V8Array继承了V8Object,因此提供了相同的存取器方法(accessor / mutator methods,相当于setter / getter方法)。除此之外,V8Array的元素也可以通过索引来进行访问。V8Object和V8Array都遵循了流式编程模型 ,这使得创建新的javascript对象变得非常简单。

V8Object player1 = new V8Object(runtime).add("name", "John");
V8Object player2 = new V8Object(runtime).add("name", "Chris");
V8Array players = new V8Array(runtime).push(player1).push(player2);
hockeyTeam.add("players", players);
player1.release();
player2.release();
players.release();

当然,再使用完V8Array之后也必须释放掉它们。


调用Javascript函数

除了执行javascript脚本,在Java中也可以使用J2V8来调用javascript函数。这些javascript函数既可以是全局的,也可以是关联到每个对象之的;既可以返回一个结果,也可以没有返回值。请看以下javascript函数:

var hockeyTeam = {
     name      : 'WolfPack',
     players   : [],                 
     addPlayer : function(player) {
                   this.players.push(player);
                   return this.players.size;
         }
}

(注意:为了在最新版本中运行这段代码,已将return this.players.size(); 修改为return this.players.size;

为了在Java中调用上述对象中的函数,我们仅仅只需要一个hockeyTeam 的句柄。通过这个对象句柄,我们可以向执行脚本一样执行函数。然而,不同与脚本的是,可以传递给函数一个V8Array作为它的参数。

V8Array parameters = new V8Array(runtime).push(player1);
int size = hockeyTeam.executeIntegerFunction("addPlayer", parameters);
parameters.release();

参数数组的元素被映射为javascript函数的参数。参数数组元素的数量和在函数中声明的参数的数量不必相符,undefined 会被作为默认的值(译注:这其实是javascript的语言特性)。最后,参数数组同样需要被手动的释放。


总结
J2V8是流行的V8 javascript引擎的一组java绑定。它为Android和其他基于java的系统带来了高效的javascript执行环境。在本教程中我们学习了如何通过J2V8来与V8引擎进行交互。特别是,我们学习了如何与V8Object,V8Array进行交互,如何执行脚本和调用javascript中的函数。

接下来我们将学习到如何注册java回调函数给javascript。


译者说明

翻译本文大多数地方都采用直译,但是中文与英文之间的语言习惯的差异不可避免,技术文章也是如此,在原文中很多概念和词句都是在一个隐含的上下文环境之中,如果直接翻译,那么就会显得非常突兀、晦涩,因此这种情况下不得不采用意译或者注释,不求“雅”,但力求“信”、“达”,同时加入了个人的理解,可能会有所偏颇,望请海涵与指正,不甚感激,也欢迎相互探讨研究。
另,以上全部代码已在Linux x86_64以及Android arm、Android x86平台上进行测试,可以正常运行。


原文作者:Ian Bull
原文连接:http://eclipsesource.com/blogs/getting-started-with-j2v8/
译者:zyzz1995
联系方式:zyzz_work@163.com
转载请保留原文以及译文出处

作者:zyzzate 发表于2016/8/25 4:19:10 原文链接
阅读:226 评论:0 查看评论

【Google官方译文】Styles and Themes

$
0
0

说明:

本文为Google官方译文,文中链接需要调整好上网姿势才能查看,原文地址Styles and Themes
希望本文能帮助到有需要的小伙伴。
译文尽量使用原来的配方,力求还是原来的味道^-^
因水平有限,难免有错误之处,欢迎指正、吐槽。

【译文开始】

style 是指定view或window外观和格式的属性集合。它可以定义诸如高度、间距、字体颜色、字体大小、背景色等更多属性。style被定义在与layout分开的xml资源文件中。

例如,通过使用style,你可以使用如下的layout文件:

<TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:textColor="#00FF00"
    android:typeface="monospace"
    android:text="@string/hello" />

然后将其转变为下面这样:

<TextView
    style="@style/CodeFont"
    android:text="@string/hello" />

所有和style相关的属性都被移除并被放入了名为CodeFont的style中,该style通过style属性进行了应用。稍后你将看到该style的定义。

theme是应用于整个Activity或application的style,而不是单个View(如上面的例子)。当style被当成theme使用时,该Activity或application中的每个View将使用每一个它所支持的style属性。例如:可以将相同的CodeFont style应用于一个Activity,那么Activity中的所有text都会有绿色的monospace字体。

定义 Styles

要创建style集合,将一个XML文件保存在工程目录res/values/下。XML文件的名字可以随意,但它必须使用xml扩展名并保存在res/values/文件夹。

XML文件的根节点必须是<resources>

对于每个要创建的style,添加一个<style>节点,它的name唯一指定了该style(该属性是必须的)。然后为style的每个属性添加一个<item>标签,它的name定义了属性,其后紧随一个value(该属性是必须的)。<item>的value可以是字符串、十六进制颜色、另一个资源类型的引用或者其他依赖于style属性的value。下面的例子展示了只有单个style的文件:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="CodeFont" parent="@android:style/TextAppearance.Medium">
        <item name="android:layout_width">fill_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:textColor">#00FF00</item>
        <item name="android:typeface">monospace</item>
    </style>
</resources>

<resources>下的每个子元素在编译期将转化为应用资源对象,通过<style>name属性的value可以引用它们。该style例子可以用@style/CodeFont在XML布局中引用(如上面的介绍)。

<style>中的parent属性是可选的,它指定了该style应该继承属性的另一个style资源ID。如果你想的话,可以重写继承的style属性。

谨记,你想以theme形式用在Activity或application上的style和用在View上的style是完全一样在XML中定义的。一个style,比方说上述的style,能以style的形式用于单个View或以them的形式用于整个Activity或application。后续将讨论怎样将style用于单个View上或者作为applicaition theme。

继承

<style>中的parent属性可让你的style指定一个继承其属性的style。你可以从现有的style继承属性,只定义那些你想修改或增加的属性。可以从自己创建的styles继承,也可以选择系统自带的(继承系统自带style的信息请看下面)。例如,你可以继承Android默认的text appearance并修改它:

<style name="GreenText" parent="@android:style/TextAppearance">
        <item name="android:textColor">#00FF00</item>
    </style>

如果你想继承自己定义的style,可以不使用parent属性。作为替代,仅仅将被继承的style的name作为新style的name的前缀。例如,要创建继承自上述CodeFont的新style,但要使颜色变红,编辑如下的新style:

  <style name="CodeFont.Red">
        <item name="android:textColor">#FF0000</item>
    </style>

注意,<style>标签中没有parent属性,因为name属性以CodeFont这个style的name开始(CodeFontstyle已经被创建),这个新style继承了CodeFont的所有属性。然后重写android:textColor属性将text变红。你可以用@style/CodeFont.Red来引用这一新style。

通过点号将names链接,你可以随意地继续继承。例如,你可以继承CodeFont.Red使其成为bigger,就像这样:

 <style name="CodeFont.Red.Big">
        <item name="android:textSize">30sp</item>
    </style>

这样就同时继承了CodeFontCodeFont.Red两个styles,然后增加了android:textSize属性。

注意: 通过链接names来继承这一技术只适用于自己定义的styles,系统自带的styles不支持这么做。要引用系统自带的style,如TextAppearance,必须使用parent属性。

Style 属性

既然你已经明白了style如何定义,你需要了解<item>节点可以定义哪些style属性。有些可能你已经知道,如layout_widthtextColor。当然,你可以使用更多的style属性。

寻找特定View属性的最佳之处就是对应的类参考,那儿列出了所有支持的XML属性。例如,所有列在TextView 的XML attributes表格中的属性可用于定义TextView style(或其子类)。参考中有一个属性android:inputType,所以你可以将其放入一个<EditText>节点,就像这样:

<EditText
    android:inputType="number"
    ... />

作为替代,你可以为EditText新建一个包含这个属性的style:

<style name="Numbers">
  <item name="android:inputType">number</item>
  ...
</style>

然后布局文件就可以实现这一style:

<EditText
    style="@style/Numbers"
    ... />

这个简单的例子看起来意味着更多的工作,但当你增加更多的属性并将这个style复用在多处时,回报将是巨大的。

所有可用style属性的参考,可以查看R.attr。记住,不是所有的View对象都接受相同的style属性,所以一般应参考特定View支持的属性。但是,如果一个View不支持应用于其上style中的所有属性,那这个View只会应用那些它支持的属性,其它的属性会被自动忽略。

有些style属性是所有View元素都不支持的,只能当作theme使用。这些style属性应用于整个window而非任一种View。如用于theme的style属性:隐藏标题栏、隐藏状态栏、改变window背景色。这些style属性不属于任一View对象。要知道这些theme专用的style属性,查看R.attr中那些以window开头的属性。例如,windowNoTitlewindowBackground是只在style当做theme用于Activity或application时才起作用的style属性。下一节介绍将style用作theme。
注意 :别忘了在每个<item>元素中对属性名加上android:命名空间这个前缀。例如:
<item name="android:inputType">

将Styles和Themes应用于UI

有两种设置style的方法:

  • 对于单个View,通过在布局XML文件中的View节点上增加style属性。
  • 或者,对于整个Activity或application,通过在Android manifest的<activity><application>节点上增加android:theme属性。

当你在布局中对单个View应用style,该style中定义的属性只用于该View。如果style被用于一个ViewGroup,子View不会继承这些style属性,只有你直接应用style的节点会发生作用。但是,你可以将style用作theme,这样就能作用于所有View节点。

为了将style用作theme,你必须在Android manifest中将该style应用于一个Activity或application。这么做之后,该Activity或application中的每个View都会应用它所支持的属性。例如,如果将之前例子中的CodeFont style用于一个Activity,那所有支持该text style属性的View节点都会应用这些属性。不支持这些属性的View则会忽略这些属性。如果一个View只支持其中的部分属性,那它只会应用那些属性。

将style用于View

下面展示了如何在布局中将style用于一个View:

<TextView
    style="@style/CodeFont"
    android:text="@string/hello" />

现在这个TextView会应用名为CodeFont 的style。(见上面 定义属性 中的例子)

注意:style属性不需要android:命名空间这一前缀。

将theme用于一个Activity或application

要将一个theme用在应用的所有activities 中,打开AndroidManifest.xml文件并在<application>标签中加入指定style名的android:theme属性。例如:

<application android:theme="@style/CustomTheme">

如果你只想在一个Activity中应用theme,就在相应的<activity>标签中增加android:theme属性。

正如Android提供了其他自带资源,你也可以使用许多自带的themes,而不用自己写。例如,你可以使用Dialog theme使你的Activity像一个dialog box:

<activity android:theme="@android:style/Theme.Dialog">

或者,你想让背景透明,可以使用Translucent theme:

<activity android:theme="@android:style/Theme.Translucent">

如果你喜欢一个theme,但想改变它,只需将该theme添加为你定制theme的parent。例如,你可以改变传统的light theme以使用自己的颜色,就像这样:

<color name="custom_theme_color">#b0b0ff</color>
<style name="CustomTheme" parent="android:Theme.Light">
    <item name="android:windowBackground">@color/custom_theme_color</item>
    <item name="android:colorBackground">@color/custom_theme_color</item>
</style>

现在可以在Android Manifest中使用CustomTheme代替Theme.Light了:

<activity android:theme="@style/CustomTheme">

根据系统版本选择theme

较新的Android版本上有额外的themes,你可能想在这些较新的系统上使用这些themes,同时兼容旧系统。要做到这一点,你可以定制一个theme,它能根据系统版本使用资源选择来切换不同parent themes

例如,这里有一个定制theme的声明,它很简单,只是标准系统中的默认light theme。它将出现在res/values下的XML文件中( 通常是res/values/styles.xml):

<style name="LightThemeSelector" parent="android:Theme.Light">
    ...
</style>

当运行在Android 3.0(API Level 11)或更高版本上时,如果想让这个theme使用较新的holographic theme,可以在res/values-v11下的XML文件中放置该theme的一个替代声明,它要使用holographic theme作为parent theme

<style name="LightThemeSelector" parent="android:Theme.Holo.Light">
    ...
</style>

现在像别的theme一样使用它,当运行在Android 3.0或更高版本上时,应用会自动切换到holographic theme

你可以在 R.styleable.Theme找到能用在themes中的标准属性清单。

更多关于提供可选资源 的信息,例如基于系统版本或其他设备配置的themes和layouts,请查看 Providing Resources 文档。

使用系统Styles和Themes

Android系统提供了大量你可应用的styles和themes。你可以在R.style类中找到所有可用styles的参考。要使用其中的styles,用点号替代style名称中的下划线。例如,可以用"@android:style/Theme.NoTitleBar"来使用Theme_NoTitleBartheme。

但是,R.style没有很好地文档化,也没有充分地描述这些styles,所以查看这些styles和themes的真实源码会对它们能提供的style属性有更好的理解。需要Android styles and themes的更棒的参考,请查看下面的源码:

这些文档可以通过示例来帮助你学习。例如,在Android themes源码中,你可以看到<style name="Theme.Dialog">的声明。在这个声明中,你能找到被系统用来形成dialogs的所有属性。

更多关于XML文件中styles and themes语法的信息,请查看Style Resource 文档。

要查看那些你能用于定义style或theme的style属性(如"windowBackground""textAppearance"),请查看R.attr或你正为其创建style的各自View类。

【译文结束】

作者:recordGrowth 发表于2016/8/25 7:39:56 原文链接
阅读:172 评论:0 查看评论

Android官方开发文档Training系列课程中文版:后台加载数据之使用CursorLoader进行查询

$
0
0

原文地址:http://android.xsoftlab.net/training/load-data-background/index.html

引言

在ContentProvider中查询数据是需要花点时间的。如果你直接在Activity进行查询,那么这可能会导致UI线程阻塞,并会引起”Application Not Responding”异常。就算不会发生这些事情,那么用户也能感觉到卡顿,这会非常恼人的。为了避免这样的问题,应该将查询的工作放在单独的线程中执行,然后等待它执行完毕后将结果显示出来。

你可以使用一个异步查询对象在后台查询,然后等查询结束之后再与Activity建立连接。这个对象就是我们要说的CursorLoaderCursorLoader除了可以进行基本查询之外,还可以在数据发生变化后自动的重新进行查询。

这节课主要会学习如何使用CursorLoader在后台进行查询。

使用CursorLoader进行查询

CursorLoader对象在后台运行着一个异步查询,当查询结束之后会将结果返回到Activity或FragmentActivity。这使得查询在进行的过程中Activity或FragmentActivity还可以继续与用户交互。

定义使用CursorLoader的Activity

如果要在Activity中使用CursorLoader,需要用到LoaderCallbacks接口。CursorLoader会调用该接口中的方法,从而使得与Activity产生交互。这节课与下节课都会详细描述该接口中的回调。

举个例子,下面的代码演示了如何定义一个使用了CursorLoaderFragmentActivity。通过继承FragmentActivity,你可以获得CursorLoader对Fragment的支持:

public class PhotoThumbnailFragment extends FragmentActivity implements
        LoaderManager.LoaderCallbacks<Cursor> {
...
}

初始化查询

使用LoaderManager.initLoader()可以初始化查询。它其实初始化了后台查询框架。可以将初始化这部分工作放在用户输入了需要查询的数据之后,或者如果不需要用户输入数据,那么也可以将这部分工作放在onCreate()onCreateView()中执行:

    // Identifies a particular Loader being used in this component
    private static final int URL_LOADER = 0;
    ...
    /* When the system is ready for the Fragment to appear, this displays
     * the Fragment's View
     */
    public View onCreateView(
            LayoutInflater inflater,
            ViewGroup viewGroup,
            Bundle bundle) {
        ...
        /*
         * Initializes the CursorLoader. The URL_LOADER value is eventually passed
         * to onCreateLoader().
         */
        getLoaderManager().initLoader(URL_LOADER, null, this);
        ...
    }

Note: getLoaderManager()方法只对Fragment类可用。如果需要在FragmentActivity中获得LoaderManager,调用getSupportLoaderManager()方法即可。

开始查询

后台查询框架的初始化一旦完成,紧接着你所实现的onCreateLoader()就会被调用。如果要启动查询,需要在该方法内返回一个CursorLoader对象。你可以实例化一个空的CursorLoader,然后再使用它的方法定义查询,或者你也可以在实例化CursorLoader的时候定义查询。

/*
* Callback that's invoked when the system has initialized the Loader and
* is ready to start the query. This usually happens when initLoader() is
* called. The loaderID argument contains the ID value passed to the
* initLoader() call.
*/
@Override
public Loader<Cursor> onCreateLoader(int loaderID, Bundle bundle)
{
    /*
     * Takes action based on the ID of the Loader that's being created
     */
    switch (loaderID) {
        case URL_LOADER:
            // Returns a new CursorLoader
            return new CursorLoader(
                        getActivity(),   // Parent activity context
                        mDataUrl,        // Table to query
                        mProjection,     // Projection to return
                        null,            // No selection clause
                        null,            // No selection arguments
                        null             // Default sort order
        );
        default:
            // An invalid id was passed in
            return null;
    }
}

一旦后台查询框架获得了该对象,那么它会马上在后台开始查询。当查询结果完成,后台查询框架会调用onLoadFinished(),该方法的具体内容会在下节课说明。

作者:u011064099 发表于2016/8/25 8:50:05 原文链接
阅读:172 评论:0 查看评论

iOS下JS与OC互相调用(四)--JavaScriptCore

$
0
0

前面讲完拦截URL的方式实现JS与OC互相调用,终于到JavaScriptCore了。它是从iOS7开始加入的,用 Objective-C 把 WebKit 的 JavaScript 引擎封装了一下,提供了简单快捷的方式与JavaScript交互。
关于JavaScriptCore的使用有两篇很好的文章:
NSHipster中文版的Java​Script​Core
iOS7 新JavaScriptCore框架入门介绍

看了上述两篇文章,对JavaScriptCore应该已经基本了解了。我就简要介绍一下,然后用代码来实际操作了。先上最终实现的效果:

效果gif

1、简要介绍JavaScriptCore

JavaScriptCore是一个iOS 7 新添加的框架,使用前需要先导入JavaScriptCore.framework
然后我们在JavaScriptCore.h中可以看到,该框架主要的类就只有五个:

JavaScriptCore.h

* 1.1 JSVirtualMachine *
JSVirtualMachine看名字直译是JS 虚拟机,也就是说JavaScript是在一个虚拟的环境中执行,而JSVirtualMachine为其执行提供底层资源。

翻译这段描述:一个JSVirtualMachine实例,代表一个独立的JavaScript对象空间,并为其执行提供资源。它通过加锁虚拟机,保证JSVirtualMachine是线程安全的,如果要并发执行JavaScript,那我们必须创建多个独立的JSVirtualMachine实例,在不同的实例中执行JavaScript

通过alloc/init就可以创建一个新的JSVirtualMachine对象。但是我们一般不用新建JSVirtualMachine对象,因为创建JSContext时,如果我们不提供一个特性的JSVirtualMachine,内部会自动创建一个JSVirtualMachine对象。

* 1.2 JSContext *
JSContext是为JavaScript的执行提供运行环境,所有的JavaScript的执行都必须在JSContext环境中。JSContext也管理JSVirtualMachine中对象的生命周期。每一个JSValue对象都要强引用关联一个JSContext。当与某JSContext对象关联的所有JSValue释放后,JSContext也会被释放。
创建一个JSContext对象的方式有:

// 1.这种方式需要传入一个JSVirtualMachine对象,如果传nil,会导致应用崩溃的。
JSVirtualMachine *JSVM = [[JSVirtualMachine alloc] init];
JSContext *JSCtx = [[JSContext alloc] initWithVirtualMachine:JSVM];

// 2.这种方式,内部会自动创建一个JSVirtualMachine对象,可以通过JSCtx.virtualMachine
// 看其是否创建了一个JSVirtualMachine对象。
JSContext *JSCtx = [[JSContext alloc] init];

// 3. 通过webView的获取JSContext。
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

上面推荐的两篇文章以及网上介绍JavaScriptCore的文章多是通过1和2这两种方式创建JSContext,然后执行JavaScript,演示JavaScriptCore。我一直有疑问,如果不是HTML结合OC,才会使用到JavaScript,那在一个虚拟的环境里运行JS有上面意义。
所以,后面我是用方式3来创建JSContext。

* 1.3 JSValue *
JSValue都是通过JSContext返回或者创建的,并没有构造方法。JSValue包含了每一个JavaScript类型的值,通过JSValue可以将Objective-C中的类型转换为JavaScript中的类型,也可以将JavaScript中的类型转换为Objective-C中的类型。
上述两篇文章中均有OC、JSValue、JavaScript的类型对应关系表。
对应关系

* 1.4 JSManagedValue *
JSManagedValue主要用途是解决JSValue对象在Objective-C 堆上的安全引用问题。把JSValue 保存进Objective-C 堆对象中是不正确的,这很容易引发循环引用,而导致JSContext不能释放。
这个类主要是将JSValue对象转换为JSManagedValue的API,而且也不常用,就不做具体介绍了。以后遇到使用场景再补充。

* 1.5 JSExport *
JSExport是一个协议类,但是该协议并没有任何属性和方法。
怎么使用呢?
我们可以自定义一个协议类,继承自JSExport。无论我们在JSExport里声明的属性,实例方法还是类方法,继承的协议都会自动的提供给任何 JavaScript 代码。
So,我们只需要在自定义的协议类中,添加上属性和方法就可以了。

2、代码操作展示

因为该系列主要是JS与OC互调,所以主要介绍如何用JavaScriptCore实现JS与OC互调。

2.1 创建UIWebView,并加载本地HTML。

这步跟 文章(一)中的步骤一是一样的。

    self.webView = [[UIWebView alloc] initWithFrame:self.view.frame];
    self.webView.delegate = self;
    NSURL *htmlURL = [[NSBundle mainBundle] URLForResource:@"index.html" withExtension:nil];
//    NSURL *htmlURL = [NSURL URLWithString:@"http://www.baidu.com"];
    NSURLRequest *request = [NSURLRequest requestWithURL:htmlURL];

    // 如果不想要webView 的回弹效果
    self.webView.scrollView.bounces = NO;
    // UIWebView 滚动的比较慢,这里设置为正常速度
    self.webView.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
    [self.webView loadRequest:request];
    [self.view addSubview:self.webView];

HTML的内容也大致一样,不过JS的调用有些区别,更简单了。

function shareClick() {
    share('测试分享的标题','测试分享的内容','url=http://www.baidu.com');
}

function shareResult(channel_id,share_channel,share_url) {
    var content = channel_id+","+share_channel+","+share_url;
    asyncAlert(content);
    document.getElementById("returnValue").value = content;
}

function locationClick() {
    getLocation();
}

function setLocation(location) {
    asyncAlert(location);
    document.getElementById("returnValue").value = location;
}

更详细的可以看demo中的HTML源码,demo地址在文章末。

2.2 添加JS要调用的原生OC方法

在HMTL加载成功的回调方法- (void)webViewDidFinishLoad:(UIWebView *)webView中添加要调用的原生OC方法。

#pragma mark - UIWebViewDelegate
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    NSLog(@"webViewDidFinishLoad");

    [self addCustomActions];
}

将所有要添加的功能方法,集中到一个方法addCustomActions中,便于维护。

#pragma mark - private method
- (void)addCustomActions
{
    JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

    [self addScanWithContext:context];

    [self addLocationWithContext:context];

    [self addSetBGColorWithContext:context];

    [self addShareWithContext:context];

    [self addPayActionWithContext:context];

    [self addShakeActionWithContext:context];

    [self addGoBackWithContext:context];
}

然后每一个小功能独立开来,这样修改和解决Bug的时候能够快速定位到某个功能。

- (void)addShareWithContext:(JSContext *)context
{
    __weak typeof(self) weakSelf = self;
    context[@"share"] = ^() {
        NSArray *args = [JSContext currentArguments];

        if (args.count < 3) {
            return ;
        }

        NSString *title = [args[0] toString];
        NSString *content = [args[1] toString];
        NSString *url = [args[2] toString];
        // 在这里执行分享的操作...

        // 将分享结果返回给js
        NSString *jsStr = [NSString stringWithFormat:@"shareResult('%@','%@','%@')",title,content,url];
        [[JSContext currentContext] evaluateScript:jsStr];
    };
}

注意:
* 1.JS要调用的原生OC方法,可以在viewDidLoad webView被创建后就添加好,但最好是在网址加载成功后再添加,以避免无法预料的乱入Bug。
* 2.block 中的执行环境是在子线程中。奇怪的是竟然可以更新部分UI,例如给view设置背景色,调用webView执行js等,但是弹出原生alertView就会在控制台报子线程操作UI的错误信息。
* 3.避免循环引用,因为block 会持有外部变量,而JSContext也会强引用它所有的变量,因此在block中调用self时,要用__weak 转一下。而且在block内不要使用外部的context 以及JSValue,都会导致循环引用。如果要使用context 可以使用[JSContext currentContext]。当然我们可以将JSContext 和JSValue当做block的参数传进去,这样就可以使用啦。

2.3 OC调用JS方法

OC调用JS方法就有多种方式了。首先介绍使用JavaScriptCore框架的方式。
* 方式1 *
使用JSContext的方法-evaluateScript,可以实现OC调用JS方法。
下面是一个调用JS中payResult方法的示例代码:

NSString *jsStr = [NSString stringWithFormat:@"payResult('%@')",@"支付成功"];
[[JSContext currentContext] evaluateScript:jsStr];

* 方式2 *
使用JSValue的方法-callWithArguments,也可以实现OC调用JS方法。
下面这个示例代码依然是调用JS中的payResult:

JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

[context[@"payResult"] callWithArguments:@[@"支付弹窗"]];

当然,如果是在执行原生OC方法之后,想要在OC执行完操作后,将结果回调给JS时,可以这样写:

- (void)addPayActionWithContext:(JSContext *)context
{
    context[@"payAction"] = ^() {
        NSArray *args = [JSContext currentArguments];

        if (args.count < 4) {
            return ;
        }

        NSString *orderNo = [args[0] toString];
        NSString *channel = [args[1] toString];
        long long amount = [[args[2] toNumber] longLongValue];
        NSString *subject = [args[3] toString];

        // 支付操作
        NSLog(@"orderNo:%@---channel:%@---amount:%lld---subject:%@",orderNo,channel,amount,subject);
        // 将支付结果返回给js
        [[JSContext currentContext][@"payResult"] callWithArguments:@[@"支付成功"]];
    };
}
方式3

以前介绍过的,利用UIWebView的API。

NSString *jsStr = [NSString stringWithFormat:@"payResult('%@')",@"支付成功"];
[weakSelf.webView stringByEvaluatingJavaScriptFromString:jsStr];

3、补充介绍JavaScriptCore

好处:使用JavaScriptCore,JS调用Native方法时,参数的传递更方便,不用担心特殊符号的转换问题。
不好的地方:只能使用在iOS 7以上。这点我相信现在基本没有多少应用还兼容iOS 6了吧,我去年在做这个功能的时候,还要兼容iOS 6 �� �� 。

先把JS与OC互调部分的介绍完了,这里再补充一些关于JavaScriptCore的相关知识。
在OC中如何往JS环境中添加一个变量,便于后续在JS中使用呢?

JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"var arr = [3, 4, 'abc'];"];

而用到实际的UIWebView上,可以这样:

JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
[context evaluateScript:@"var arr = [3, 4, 'abc'];"];

当上面这两行代码执行完后,我点击HTML中的按钮

<input type="button" value="输出arr" onclick="showArr()" />
function showArr(){
     asyncAlert(arr);
}

function asyncAlert(content) {
     setTimeout(function(){
               alert(content);
         },1);
}

直接输出arr,结果是这样的:

如果我们在OC中想要取出arr,只需要这样:

JSValue *value = context[@"arr"];

OC中的block可以传入到JavaScript中,这样就创建了一个新的JS方法。我们上面的JS调用OC方法,就是利用的这个实现的。

关于JSExport如何使用?
JSExport 主要是用于将OC中定义的Model类等引入到JavaScript中,便于在JS中使用这种对象和对象的属性、方法。
JSExport的大致使用流程是:
1.创建一个自定义协议XXXExport 继承自JSExport
2.在自定义的XXXExport中添加JS里需要调用的属性和方法。
3.在自定义的Model类中实现XXXExport中的属性的get/set方法以及定义的方法。
4.通过JSContext将Model类或者Model类的实例插入到JavaScript中。

当然,我们也可以给已经存在的类动态添加协议,来使其可以供JS 使用。这些示例和示例代码,在文章NSHipster中文版的Java​Script​CoreJavaScriptCore框架在iOS7中的对象交互和管理中有很详细的介绍和使用展示。

WKWebView 与JavaScriptCore

关于WKWebView 与JavaScriptCore,由于WKWebView 不支持通过如下的KVC的方式创建JSContext:

JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

那么就不能在WKWebView中使用JavaScriptCore了。
而且,WKWebView中有OC 和JS交互的方式,更easy 、更简洁,因此也用不着使用JavaScriptCore。
WKWebView中如何实现OC与JS交互可以看前面这篇文章:iOS下JS与OC互相调用(三)–MessageHandler

UIWebView利用JavaScriptCore来实现交互的示例工程:JS_OC_JavaScriptCore

Have Fun!

作者:u011619283 发表于2016/8/25 8:50:54 原文链接
阅读:153 评论:0 查看评论

Android 的String资源格式化方法

$
0
0
很多时候我们感性Google在设计Android时遵守了大量MVC架构方式,可以让写公共代码、美工和具体逻辑开发人员独立出来。有关Android 的资源文件values/strings.xml中如何实现格式化字符串呢?
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">stringdemo</string>
    <string name="hello">hello robert</string>
</resources>
上面是一段简单的字符串资源文件,没有用到格式化,因为比较简单直接描述了意思,当我们设计一个类似 Delete xxx File ? 的时候,我们可能需要在Java中动态获取 xxx 的名称,所以定义资源时使用格式化可以轻松解决,不需要一堆String去拼接或StringBuffer一个一个append这样的愚蠢方法,看例子
<string name="alert">Delete %1$s File</string>
这里%1$s代表这是一个字符串型的,如果是整数型可以写为%1$d,类似printf这样的格式化字符串函数,当然如果包含了多个需要格式化的内容,则第二个可以写为%2$s或%2$d了,那么最终在Java中如何调用呢? 看下面的例子:

例一: 整数型的
<string name="alert">I am %1$d years old</string>
定义的是这样的
当然,我们杜绝意外情况,比如冒出个secret这样的string类型的,注意上面是%1$d不是%1$s,所以默认标准的合并成为
int nAge=23;
String sAgeFormat = getReSsources().getString(R.string.alert);
String sFinalAge = String.format(sAgeFormat, nAge);

这样执行完后,就组成了 I am 23 years old。

也可以直接获取

 String sFinalAge=getResources().getString(R.string.alert,nAge);
下面看下String字符串时的情况.

例二: 字符串型的
String sName="robert";
<pre name="code" class="java">String sCity="Yantai";


资源定义为
<string name="alert2">My name is %1$s , I am form %2$s</string>
则Java中只需要
String sInfoFormat = getResources().getString(R.string.alert2);
String sFinalInfo=String.format(sInfoFormat, sName, sCity);

本行最终sFinalInfo显示的内容为

My name is robert, I am form Yantai


欢迎扫描二维码,关注公众号



作者:robertcpp 发表于2016/8/25 9:27:49 原文链接
阅读:265 评论:0 查看评论

Android开发学习之路--逆向分析反编译

$
0
0

  一般情况下我们想要了解别人的app怎么实现这个动画,这个效果的时候,总是会想到反编译一下,看下布局,看下代码实现。对,这对于有经验的玩家确实手到擒来了,但是初学者,根本就不知道怎么反编译,怎么看代码,甚至不知道什么是反编译。那就学一下吧。


简单写一个app

  先简单写个app用作后面的反编译,当然可以直接拿现有的比较成熟的app,但是没有源码我们没办法好好比较了。好了,比较简单就直接上代码了,这里用了下databinding,具体以后也会写文章具体讲解databinding的。

xml界面代码:

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data class="MainDataBinding">
    </data>

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello Decompilation:"
            android:textSize="20sp" />

        <EditText
            android:id="@+id/et_account"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:gravity="center"
            android:hint="@string/account"/>

        <EditText
            android:id="@+id/et_password"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:gravity="center"
            android:hint="@string/password"/>

        <Button
            android:id="@+id/bt_login"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:text="@string/login"
            android:textAllCaps="false" />
    </LinearLayout>
</layout>

java代码:

package com.jared.decompilationstudy;

import android.databinding.DataBindingUtil;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;

import com.jared.decompilationstudy.databinding.MainDataBinding;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private MainDataBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(MainActivity.this, R.layout.activity_main);

        binding.btLogin.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.bt_login:
                if (checkInfo()) {
                    Toast.makeText(MainActivity.this, getResources().getString(R.string.login_ok),Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(MainActivity.this, getResources().getString(R.string.login_failure),Toast.LENGTH_SHORT).show();
                }
                break;
        }
    }

    private boolean checkInfo() {
        if (!"admin".equals(binding.etAccount.getText().toString()))
            return false;
        if (!"123456".equals(binding.etPassword.getText().toString()))
            return false;
        return true;
    }
}

  其实主要实现就是一个简单的登录界面,判断用户名为admin,密码为123456才会显示登录成功。后面也会通过反编译之后重新打包破解之。那就继续吧。


Apktool工具–反编译资源

  apktool工具是反编译资源用的,当然你也可以把apk的后缀名改为zip,然后解压文件,但是直接解压出来的文件只有图片资源可用,其他的都是乱码,为了查看layout等资源,所以我们就需要apktool工具了。
  下载地址:http://ibotpeaches.github.io/Apktool/install/

  apktool工具主要有三个文件,分别是aapt,apktool,apktools.jar。以mac为例,将三个文件拷贝到/usr/local/bin/目录下,必要的情况下设置可执行权限。之后在终端可以执行apktool,有如下信息表示ok。

Apktool v2.1.1 - a tool for reengineering Android apk files
with smali v2.1.2 and baksmali v2.1.1
Copyright 2014 Ryszard Wiśniewski <brut.alll@gmail.com>
Updated by Connor Tumbleson <connor.tumbleson@gmail.com>

usage: apktool
 -advance,--advanced   prints advance information.
 -version,--version    prints the version then exits
usage: apktool if|install-framework [options] <framework.apk>
 -p,--frame-path <dir>   Stores framework files into <dir>.
 -t,--tag <tag>          Tag frameworks using <tag>.
usage: apktool d[ecode] [options] <file_apk>
 -f,--force              Force delete destination directory.
 -o,--output <dir>       The name of folder that gets written. Default is apk.out
 -p,--frame-path <dir>   Uses framework files located in <dir>.
 -r,--no-res             Do not decode resources.
 -s,--no-src             Do not decode sources.
 -t,--frame-tag <tag>    Uses framework files tagged by <tag>.
usage: apktool b[uild] [options] <app_path>
 -f,--force-all          Skip changes detection and build all files.
 -o,--output <dir>       The name of apk that gets written. Default is dist/name.apk
 -p,--frame-path <dir>   Uses framework files located in <dir>.

For additional info, see: http://ibotpeaches.github.io/Apktool/
For smali/baksmali info, see: https://github.com/JesusFreke/smali

  至于没有成功的,这里也不讲解了,相信google会给你答案。

  接着我们开始反编译资源了。先把之前的android代码打包成decompilation.apk。执行如下命令:

apktool d decompilation.apk
I: Using Apktool 2.1.1 on decompilation.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /Users/jared/Library/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...

  有时候会出现问题类似如下:

Exception in thread "main" brut.androlib.err.UndefinedResObject: resource spec: 0x01010462

  原因可能你的apktool版本很老,下载最新的,还有就是需要删除下/Users/用户名/Library/apktool/framework/1.apk

  反编译成功后,会在同级目录下生成decompilation,cd进入decompilation目录,ls查看内容如下,有AndroidManifest.xml文件,res下就是我们需要的资源文件了,smali就是Dalvik的一些指令代码,之后有机会再学习学习。

->decompilation ls
AndroidManifest.xml original            smali
apktool.yml         res                 unknown

  我们看下AndroidManifest.xml的内容:

<?xml version="1.0" encoding="utf-8" standalone="no"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.jared.decompilationstudy" platformBuildVersionCode="23" platformBuildVersionName="6.0-2438415">
       <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme ="@style/AppTheme">
           <activity android:name="com.jared.decompilationstudy.MainActivity">
               <intent-filter>
                   <action android:name="android.intent.action.MAIN"/>
                   <category android:name="android.intent.category.LAUNCHER"/>
               </intent-filter>
           </activity>
      </application>
  </manifest>

  在对比下源码Manifest.xml的内容:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jared.decompilationstudy">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

  基本上保持了一致,这里我们没法看java源码,只是资源,那可以看源码吗?答案是肯定的,接着学习吧。


dex2jar&jd-gui工具–反编译源码

  dex2jar是把dex文件反编译为jar文件,jd-gui是将jar文件转换为java代码。
  dex2jar下载地址:http://sourceforge.net/projects/dex2jar/files/
  jd-gui下载地址:http://jd.benow.ca/

  dex2jar下载后是一个目录,内容如下:

➜  dex2jar-2.0 ls
classes-dex2jar.jar            d2j-jar2jasmin.bat
classes.dex                    d2j-jar2jasmin.sh
d2j-baksmali.bat               d2j-jasmin2jar.bat
d2j-baksmali.sh                d2j-jasmin2jar.sh
d2j-dex-recompute-checksum.bat d2j-smali.bat
d2j-dex-recompute-checksum.sh  d2j-smali.sh
d2j-dex2jar.bat                d2j-std-apk.bat
d2j-dex2jar.sh                 d2j-std-apk.sh
d2j-dex2smali.bat              d2j_invoke.bat
d2j-dex2smali.sh               d2j_invoke.sh
d2j-jar2dex.bat                lib
d2j-jar2dex.sh

  这里我们需要的是d2j-dex2jar.sh脚本。至于jd-gui的话,就是安装好就行了,和一般的ide差不多的。

  下面就开始反编译源码了。首先需要把decompilation.apk改为decompilation.zip,然后解压缩得到classes.dex文件。然后把classes.dex拷贝到dex2jar目录下:

➜  dex2jar-2.0 cp ../apk/decompilation2/classes.dex .
➜  dex2jar-2.0 ls
classes-dex2jar.jar            d2j-jar2jasmin.bat
classes.dex                    d2j-jar2jasmin.sh
d2j-baksmali.bat               d2j-jasmin2jar.bat
d2j-baksmali.sh                d2j-jasmin2jar.sh
d2j-dex-recompute-checksum.bat d2j-smali.bat
d2j-dex-recompute-checksum.sh  d2j-smali.sh
d2j-dex2jar.bat                d2j-std-apk.bat
d2j-dex2jar.sh                 d2j-std-apk.sh
d2j-dex2smali.bat              d2j_invoke.bat
d2j-dex2smali.sh               d2j_invoke.sh
d2j-jar2dex.bat                lib
d2j-jar2dex.sh

  开始反编译了,执行如下所示:

➜  dex2jar-2.0 ./d2j-dex2jar.sh classes.dex --force
dex2jar classes.dex -> ./classes-dex2jar.jar
➜  dex2jar-2.0 ls
classes-dex2jar.jar

  执行完后就生成了classes-dex2jar.jar文件。接着我们用jd-gui看下源码,打开jd-gui软件,打开classes-dex2jar.jar文件如下所示:

  这个时候你可能会非常爽,可以看到源码了,当然也会fuck,辛辛苦苦写的代码就这样被盗了。其实一般app都会做混淆的,看得不是那么容易的,这里没有做混淆就很直白了。好了,基本上一个app的反编译分析也基本上到此结束了。


破解apk,重新打包

  这里仅当做技术学习,毕竟别人也是辛辛苦苦写的代码,好了,继续吧。
  上面已经反编译了资源,我们回到decompilation目录下,这里有smali目录,主要是一个davik指令的代码。

decompilation ls
AndroidManifest.xml original            smali
apktool.yml         res                 unknown
➜  decompilation  cd smali
➜  smali ls
android com
➜  smali cd comcom ls
android jared
➜  com cd jared/decompilationstudy/
➜  decompilationstudy ls
BR.smali           R$bool.smali       R$integer.smali    R$styleable.smali
BuildConfig.smali  R$color.smali      R$layout.smali     R.smali
MainActivity.smali R$dimen.smali      R$mipmap.smali     databinding
R$anim.smali       R$drawable.smali   R$string.smali
R$attr.smali       R$id.smali         R$style.smali

  那我们要怎么破解呢?逐个击破吧,先看“登录成功”和“登录失败”,我们已“登录成功”为破解的开始吧:

➜  decompilation grep -nr "登录成功" res
res/values/strings.xml:39:    <string name="login_ok">登录成功</string>

  可以得知这个string的name为login_ok。然后我们继续查找这个login_ok怎么来的?

➜  decompilationstudy grep -nr 'login_ok' .
./R$string.smali:88:.field public static final login_ok:I = 0x7f060024
➜  decompilationstudy grep -nr '0x7f060024' .
./MainActivity.smali:117:    const v1, 0x7f060024
./R$string.smali:88:.field public static final login_ok:I = 0x7f060024

  可以得知login的I=0X7F060024,然后查找这个得到两个地方调用,一个是MainActivity.smali,后一个是string本身。显然我们的这个是在MainActivity.smali的第117行调用了。那我们继续去看看吧:

  这里需要一点汇编基础才能看的懂代码了。

# virtual methods
.method public onClick(Landroid/view/View;)V
    .locals 3
    .param p1, "view"    # Landroid/view/View;

    .prologue
    const/4 v2, 0x0

    .line 25
    invoke-virtual {p1}, Landroid/view/View;->getId()I

    move-result v0

    packed-switch v0, :pswitch_data_0

    .line 34
    :goto_0
    return-void

    .line 27
    :pswitch_0
    invoke-direct {p0}, Lcom/jared/decompilationstudy/MainActivity;->checkInfo()Z

    move-result v0

    if-eqz v0, :cond_0

    .line 28
    invoke-virtual {p0}, Lcom/jared/decompilationstudy/MainActivity;->getResources()Landroid/content/res/Resources;

    move-result-object v0

    const v1, 0x7f060024

    invoke-virtual {v0, v1}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String;

    move-result-object v0

    invoke-static {p0, v0, v2}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object v0

    invoke-virtual {v0}, Landroid/widget/Toast;->show()V

    goto :goto_0

    .line 30
    :cond_0
    invoke-virtual {p0}, Lcom/jared/decompilationstudy/MainActivity;->getResources()Landroid/content/res/Resources;

    move-result-object v0

    const v1, 0x7f060023

    invoke-virtual {v0, v1}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String;

    move-result-object v0

    invoke-static {p0, v0, v2}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object v0

    invoke-virtual {v0}, Landroid/widget/Toast;->show()V

    goto :goto_0

    .line 25
    nop

    :pswitch_data_0
    .packed-switch 0x7f0b005a
        :pswitch_0
    .end packed-switch
.end method

  可以看出来这是一个onclick方法,有一个checkInfo方法,看这行代码,if-eqz v0, :cond_0,意思是v0为0就跳转到cond_0。很显然cond_0就是登陆失败了,也就是checkInfo返回了true和false分别跳转到对应的方法中。寻着这个,我们看下checkInfo的代码:

.method private checkInfo()Z
    .locals 3

    .prologue
    const/4 v0, 0x0

    .line 37
    const-string v1, "admin"

    iget-object v2, p0, Lcom/jared/decompilationstudy/MainActivity;->binding:Lcom/jared/decompilationstudy/databinding/MainDataBinding;

    iget-object v2, v2, Lcom/jared/decompilationstudy/databinding/MainDataBinding;->etAccount:Landroid/widget/EditText;

    invoke-virtual {v2}, Landroid/widget/EditText;->getText()Landroid/text/Editable;

    move-result-object v2

    invoke-virtual {v2}, Ljava/lang/Object;->toString()Ljava/lang/String;

    move-result-object v2

    invoke-virtual {v1, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

    move-result v1

    if-nez v1, :cond_1

    .line 41
    :cond_0
    :goto_0
    return v0

    .line 39
    :cond_1
    const-string v1, "123456"

    iget-object v2, p0, Lcom/jared/decompilationstudy/MainActivity;->binding:Lcom/jared/decompilationstudy/databinding/MainDataBinding;

    iget-object v2, v2, Lcom/jared/decompilationstudy/databinding/MainDataBinding;->etPassword:Landroid/widget/EditText;

    invoke-virtual {v2}, Landroid/widget/EditText;->getText()Landroid/text/Editable;

    move-result-object v2

    invoke-virtual {v2}, Ljava/lang/Object;->toString()Ljava/lang/String;

    move-result-object v2

    invoke-virtual {v1, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

    move-result v1

    if-eqz v1, :cond_0

    .line 41
    const/4 v0, 0x1

    goto :goto_0
.end method

  首先赋值v0为0x0,const/4 v0, 0x0,接着看下代码:const-string v1, “admin”,很明显是常量赋值,接着往下看:if-nez v1, :cond_1,如果结果不为0就跳转到cond_1,继续看:const-string v1, “123456”。也是常量赋值123456,然后结果为0跳转到cond_0,否则执行,const/4 v0, 0x1,goto :goto_0,就是v0赋值为1,跳转到goto_0。

  综上分析,可以得出主要的关键点是v0寄存器了,checkInfo返回的值为v0,那么如果我们把v0的初始值赋值为0x1,那么不就永远返回true了,不管什么账号登录都是ok的了。修改28行代码为:const/4 v0, 0x1。不容易啊,终于改好了,那么接着我们看看是不是如我们所愿呢?
  修改完了代码,我们把修改好的代码打包吧,执行命令如下:

➜  apk apktool b decompilation -o decompilation2.apk
I: Using Apktool 2.1.1
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
I: Checking whether resources has changed...
I: Building resources...
I: Building apk file...
I: Copying unknown files/dir...

   打包完的代码是没有签名的,没办法在手机上安装的,那么接下来我们开始重签名吧。

➜  apk keytool -genkey -v -keystore Android.keystore -alias android.keystore -keyalg RSA -validity 20000
输入密钥库口令:
再次输入新口令:
您的名字与姓氏是什么?
  [Unknown]:  1
您的组织单位名称是什么?
  [Unknown]:  1
您的组织名称是什么?
  [Unknown]:  1
您所在的城市或区域名称是什么?
  [Unknown]:  1
您所在的省/市/自治区名称是什么?
  [Unknown]:  1
该单位的双字母国家/地区代码是什么?
  [Unknown]:  1
CN=1, OU=1, O=1, L=1, ST=1, C=1是否正确?
  [否]:  y

正在为以下对象生成 2,048 位RSA密钥对和自签名证书 (SHA256withRSA) (有效期为 20,000 天):
         CN=1, OU=1, O=1, L=1, ST=1, C=1
输入 <android.keystore> 的密钥口令
        (如果和密钥库口令相同, 按回车):
[正在存储Android.keystore]

  这里偷懒了,就随便填写了内容,接着用jarsigner签名:

➜  jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore Android.keystore -storepass 123456 decompilation2.apk Android.keystore
   正在添加: META-INF/MANIFEST.MF
   正在添加: META-INF/ANDROID_.SF
   正在添加: META-INF/ANDROID_.RSA
   …………
    正在签名: com/android/databinding/library/baseAdapters/com.android.databinding.library.baseAdapters-br.bin
  正在签名: com/android/databinding/library/baseAdapters/com.android.databinding.library.baseAdapters-layoutinfo.bin
  正在签名: com/android/databinding/library/baseAdapters/com.android.databinding.library.baseAdapters-setter_store.bin
jar 已签名。

警告:
未提供 -tsa 或 -tsacert, 此 jar 没有时间戳。如果没有时间戳, 则在签名者证书的到期日期 (2071-05-29) 或以后的任何撤销日期之后, 用户可能无法验证此 jar。

  大工搞成,接着安装到手机上通过adb install decompilation2.apk。

  见证奇迹的时刻到了:

  破解成功了,你也可以试试。

作者:eastmoon502136 发表于2016/8/25 11:29:06 原文链接
阅读:221 评论:0 查看评论

Android架构(一)MVP全解析

$
0
0

前言

关于架构的文章,博主很早就想写了,虽说最近比较流行MVVM,但是MVP以及MVC也没有过时之说,最主要还是要根据业务来选择合适的架构。当然现在写MVP的文章很多,也有很多好的文章,但是大多数看完后还是一头雾水,用最少的文字表述清楚是我一贯的风格(这里小小的装逼一下),所以还是自己总结比较靠谱。

1.回顾MVC

讲到MVP前我们有必要回顾下MVC,MVC(Model-View-Controller,模型-视图-控制器)模式是80年代Smalltalk-80出现的一种软件设计模式,后来得到了广泛的应用,用一种业务逻辑、数据、界面显示分离的方法组织代码,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。

android的MVC

Android中界面部分也可以采用了MVC框架,MVC的角色定义分别为:

  • 模型层(Model)
    我们针对业务模型,建立的数据结构和相关的类,就可以理解为Model,Model是与View无关,而与业务相关的。

  • 视图层(View)
    一般采用xml文件或者java代码进行界面的描述,也可以使用javascript+html等的方式作为view层。

  • 控制层(controller)
    android的控制层通常在acitvity、Fragment或者由它们控制的其他业务类中。

android的MVC缺点

在Android开发中,Activity并不是一个标准的MVC模式中的Controller,它的首要职责是加载应用的布局和初始化用户界面,并接受并处理来自用户的操作请求,进而作出响应。随着界面及其逻辑的复杂度不断提升,Activity类的职责不断增加,以致变得庞大臃肿。

2.什么是MVP

MVP(Model View Presenter)是MVC的演化版本,MVP的角色定义分别为:

  • Presenter
    作为View和Model的沟通的桥梁,它从Model层检索数据后返回给View层,使得View和Model之间没有耦合。

  • Model
    主要提供数据的存取功能。Presenter需要通过Model层来存储、获取数据。

  • View
    负责处理用户事件和视图部分的展示。在Android中,它可能是Activity、Fragment类或者是某个View控件。

    这里写图片描述

在MVP里,Presenter完全把Model和View进行了分离,主要的程序逻辑在Presenter里实现。而且,Presenter与具体的View是没有直接关联的,而是通过定义好的接口进行交互,从而使得在变更View时候可以保持Presenter的不变。 View只应该有简单的Set/Get的方法,用户输入和设置界面显示的内容,除此就不应该有更多的内容,绝不容许直接访问Model,这就是与MVC很大的不同之处。

3.使用MVP

这里我们举个例子,通过网络获取文章的标题和内容并显示在界面上,访问网络的内容和Android网络编程(三)Volley用法全解析这篇文章所采用的数据是一样的,Json数据格式请点击这里

访问网络数据用的是OkHttpFinal,包目录如下图所示:

这里写图片描述

实现Model

首先我们要创建bean文件,这里帖上部分代码:

public class ArticleInfo {
    private String desc;
    private String status;
    private List<detail> detail = new ArrayList<detail>();

    public List<ArticleInfo.detail> getDetail() {
        return detail;
    }

    public void setDetail(List<ArticleInfo.detail> detail) {
        this.detail = detail;
    }

 ...省略

    public class detail {
        private String title;
        private String article_url;
        private String my_abstract;
        private String article_type;

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }
...省略
}

接下来是获取文章的Model接口类,这个接口用来定义如何获取数据:

public interface ArticleModel {
    void getArtcle(OnArticleListener onArticleListener);
}

里面有一个回调监听接口,里面定义了网络访问回调的各种状态:

public interface OnArticleListener {
    void onSuccess(ArticleInfo articleInfo);
    void onStart();
    void onFailed();
    void onFinish();
}

接下来我们写ArticleModel的实现类用来获取数据:

public class ArticleModelImpl implements ArticleModel {
    @Override
    public void getArtcle(final OnArticleListener onArticleListener) {
        HttpRequest.post("http://api.1-blog.com/biz/bizserver/article/list.do",new BaseHttpRequestCallback<ArticleInfo>(){
            @Override
            protected void onSuccess(ArticleInfo articleInfo) {
                super.onStart();
                onArticleListener.onSuccess(articleInfo);
            }

            @Override
            public void onStart() {
                super.onStart();
                onArticleListener.onStart();
            }

            @Override
            public void onFailure(int errorCode, String msg) {
                super.onFailure(errorCode, msg);
                onArticleListener.onFailed();
            }

            @Override
            public void onFinish() {
                super.onFinish();
                onArticleListener.onFinish();
            }

        });

    }
}

通过OkHttpFinal来获取数据,同时在回调函数中调用自己定义的回调函数。

实现Presenter

首先定义ArticlePresenter接口:

public interface ArticlePresenter {
    void getArticle();
}

实现ArticlePresenter接口:

public class ArticlePresenterImpl implements ArticlePresenter, OnArticleListener {
    private ArticleView mArticleView;
    private ArticleModel mArticleModel;
    public ArticlePresenterImpl(ArticleView mArticleView) {
        this.mArticleView = mArticleView;
        mArticleModel = new ArticleModelImpl();
    }
    @Override
    public void getArticle() {
        mArticleModel.getArtcle(this);
    }
    @Override
    public void onSuccess(ArticleInfo articleInfo) {
        mArticleView.setArticleInfo(articleInfo);
    }
    @Override
    public void onStart() {
        mArticleView.showLoading();
    }
    @Override
    public void onFailed() {
        mArticleView.showError();
    }
    @Override
    public void onFinish() {
        mArticleView.hideLoading();
    }
}

很明显ArticlePresenterImpl 中含有ArticleModel 和ArticleView的实例(后面会讲),通过实现OnArticleListener接口并调用ArticleModel 来获取数据并回调给自身,最后通过ArticleView来和Activity进行交互,来更改界面。这回我们应该明白了,Presenter就是一个中间人的角色,他通过Model来获得并保存数据,然后在通过View来更新界面。这期间通过定义接口使得View和Model没有任何交互。最后来看看View层的实现:

实现View

ArticleView用来定义界面交互的方法:

public interface ArticleView {
    void setArticleInfo(ArticleInfo articleInfo);
    void showLoading();
    void hideLoading();
    void showError();
}

我们在Activity中来调用ArticlePresenterImpl:


public class MainActivity extends BaseActivity implements ArticleView{
    private Button bt_getarticle;
    private TextView tv_article_title;
    private TextView tv_article_content;
    private ArticlePresenter mArticlePresenter;
     private Dialog mDialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }
    private void initView() {
        mArticlePresenter=new ArticlePresenterImpl(this);
        mDialog=new ProgressDialog(this);
        mDialog.setTitle("获取数据中");
        bt_getarticle = findView(R.id.bt_getarticle);
        tv_article_title = findView(R.id.tv_article_title);
        tv_article_content = findView(R.id.tv_article_content);
        bt_getarticle.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mArticlePresenter.getArticle();
            }
        });
    }
    @Override
    public void setArticleInfo(ArticleInfo articleInfo) {
        if(null!=articleInfo) {
            List<ArticleInfo.detail> list = articleInfo.getDetail();
            if(null!=list&&list.size()>1)
            tv_article_title.setText(list.get(1).getTitle());
            tv_article_content.setText(list.get(1).getMy_abstract());
        }
    }

    @Override
    public void showLoading() {
        mDialog.show();
    }
    @Override
    public void hideLoading() {
        if(mDialog.isShowing()) {
            mDialog.dismiss();
        }
    }
    @Override
    public void showError() {
        Toast.makeText(getApplicationContext(),"网络出错",Toast.LENGTH_SHORT).show();
    }

}

需要注意的是MainActivity实现了ArticleView接口,用来接收回调更新界面,很明显MainActivity并没有做其他与界面无关的事情。

4.MVP的优缺点

优点

  • 降低耦合度,实现了Model和View真正的完全分离。
  • 模块职责划分明显,层次清晰。
  • Presenter可以复用,一个Presenter可以用于多个View,而不需要更改Presenter的逻辑(当然是在View的改动不影响业务逻辑的前提下)。
  • 如果我们把逻辑放在Presenter中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试)。

缺点

  • 额外的代码复杂度及学习成本。
  • 如果Presenter过多地与特定的视图的联系过于紧密,一旦视图需要变更,那么Presenter也需要变更了。

5.总结

好了,MVP的例子就讲到这,其实还有很多种方式来实现MVP,在这里我也只是讲了一个最基础的方式,但是万变不离其中。简要总结MVP三者之间的关系是:View和Model之间没有联系,View通过接口与Presenter进行交互,Model不主动和Presenter联系,被动的等着Presenter来调用其接口,Presenter通过接口和View/Model来联系。

github源码下载

参考资料:
Android中的MVP
Android App的设计架构:MVC,MVP,MVVM与架构经验谈
Android开发中的MVP架构详解
浅谈 MVP in Android
使用MVP模式重构代码
MVP模式是你的救命稻草吗?
Android MVP 详解(上)
MVP模式的优缺点
Android MVP 实例运用

作者:itachi85 发表于2016/8/25 12:01:54 原文链接
阅读:1563 评论:5 查看评论

Android时间处理详解

$
0
0

时间术语:

Greenwich:格林威治/格林尼治,是位于伦敦市中心东南部的一个区,1675国王查理二世在此建立了皇家格林尼治天文台,1851年御用天文学家艾里在天文台设置了中星仪并确定了格林威治子午线,1884年在美国华盛顿特区举行的国际本初子午线大会上正式将此线定之为经度的起点。

GMT(Greenwich Mean Time):格林尼治标准时间/格林威治标准时间, 格林尼治标准时间的正午是指当太阳横穿格林尼治子午线时(也就是在格林尼治上空最高点时)的时间。由于地球在它的椭圆轨道里的运动速度不均匀,这个时刻可能与实际的太阳时有误差,最大误差达16分钟。由于地球每天的自转是有些不规则的,而且正在缓慢减速,因此格林尼治时间已经不再被作为标准时间使用。现在的标准时间,是由原子钟报时的协调世界(UTC)。

UTC(Coordinated Universal Time):协调世界时/世界协调时间/世界标准时间,UTC基于国际原子时,并通过不规则的加入闰秒来抵消地球自转变慢的影响,是比GMT时间更精确,也就是说一天不一定是86400秒,可能会根据情况加入一个闰秒。闰秒在必要的时候会被插入到UTC中,以保证协调世界时UTC与世界时UT1相差不超过0.9秒。UTC表示时间偏移量时,书写格式为±[hh]:[mm]或±[hh][mm]或±[hh]。UTC的0时区表示会在时间后面加一个”Z”,如“01:30Z”或“0130Z”表示0时区的1时30分,而北京时间为“09:30 UTC+08:00”或“09:30 UTC+0800”,会比标准UTC时间早8个小时。

Epoch(Unix epoch):操作系统的纪元时间,为“1970-01-01T00:00:00Z”,即格林威治当地时间1970年1月1日0时0分0秒,至于为什么会选择这个时间说法不一,最好的解释是设计Unix/类Unix操作系统时为了方便决定采用的时间,并不是Unix/类Unix系统的诞生时间。

Unix time:Unix timestamp/POSIX time/Epoch time,定义为从1970-01-01 00:00:00 UTC开始所经过的秒数。由于不考虑闰秒,所以它不是线性的时间表示,也不是真正的UTC时间表示。

Java中的时间处理:

java.util.Date

用来表示一个特定时刻,精确到毫秒(1/1000秒)。所表示的时间总是UTC时间,不考虑系统的时区。由于该类“历史悠久”,很多方法已经被废弃了。如果想使用展示日期相关的方法,建议使用java.text.DateFormat类相关方法。如果想使用计算或划分日期的方法,建议使用java.util.Calendar类相关方法。

public Date()该构造器会根据系统当前时间(System.currentTimeMillis())构造时间。
public Date(long milliseconds)该构造器会使用从1970-01-01T00:00:00Z所经过的毫秒数构造时间,即Unix时间*1000。

其他构造器不建议使用,如public Date(int year, int month, int day) @deprecated 该构造器会使用GregorianCalendar(格里高利历/公历)默认时区来构造日期,参数中的year为从1900年后多少年,如果year为0则表示1900+0=1900年。month为0 - 11,day为1 - 31。其他类似构造器:public Date(int year, int month, int day, int hour, int minute)hour为0 - 23,minute为0 - 59。public Date(int year, int month, int day, int hour, int minute, int second)second为0 - 59。public Date(String string)虽然可以解析很多格式的时间表示字符串,但是也已不建议使用了。

Date类可以很方便的进行时间比较,如public boolean after(Date date)可以判断该时间是否在给定时间之后,public boolean before(Date date)可以判断该时间是否在给定时间之前,同时也为比较实现了compareToequals方法。可以通过public long getTime()方法拿到从1970-01-01T00:00:00Z所经过的毫秒数。

其它所有方法都已经废弃了,不建议使用。

java.util.Calendar

用来让Date对象和一系列整形字段(如YEAR,MONTH)很方便的转换。Calendar是一个抽象类,其子类可以根据情况将Date解释成不同的日期形式,已知的也是我们最常用到的子类就是GregorianCalendar(格里高利历/公历)。
像其它的本地化敏感类一样,Calendar会提供getInstance一系列方法,返回基于系统设置的GregorianCalendar实例并将时间和日期初始化为当前的时间日期。
如果Calendar是宽松模式的,它可以接受比它所生成的日历字段范围更大范围内的值,如一个宽松的GregorianCalendar会把MONTH==JANUARY,DAY_OF_MONTH==32解释为2月1日,而非宽松的GregorianCalendar则会抛出字段越界异常。
Calendar使用两个参数定义了指定情况下的7 天制星期:firstDayOfWeek(每周的第一天是周几/周几作为每周的开始,通常为SUNDAY)和minimalDaysInFirstWeek(新年的第一个周所包含的最小天数,从1-7,如GregorianCalendar日历中该值为1,那么新年的第一个周就必须包含1月1日,而如果该值为7,则只有当1月1号那天的星期等于firstDayOfWeek时,1月1日所在的周才会最为新年的第一周,否则将作为上一年的最后一周,因此firstDayOfWeek和minimalDaysInFirstWeek共同决定了1月1日所在的那一周会不会成为新年的第一周)。

Calendar的字段操作:

  • set(f, value)方法可以更改f字段的值为value,而且是马上更改内部字段的值,但是calendar的milliseconds值要在下次调用get()getTime()getTimeInMillis()方法时才会重新计算,因此多次调用set()方法不会触发多次不必要的计算。由于set()方法因为修改了字段的值,其他日历字段也可能会发生更改,这取决于日历字段、日历字段的值和日历系统。此外get(f)没有必要返回计算后的字段值,因为这些细节会由具体的Calendar决定,如GregorianCalendar最初被设置为1999年8月31日。调用set(Calendar.MONTH, Calendar.SEPTEMBER)将该日期设置为1999年9月31日。如果随后调用 getTime(),将解析为1999年10月1日(因为9月没有31日)的一个暂时内部表示。但是,在调用 getTime() 之前调用 set(Calendar.DAY_OF_MONTH, 30) 会将该日期设置为 1999年9月30日,因为在调用 set() 之后没有发生马上重新计算。
  • add(f, delta)方法可以给f字段的值设置delta偏移,相当于set(f, get(f) + delta)。为了防止add(f, delta)方法产生相应字段值地溢出,约定两个规则:
    规则一:如果设置了delta后f字段的值发生了溢出,则将该字段的值取模以调整回取值范围内,并且下一个更大的字段的值会相应的增加或减少。
    规则二:如果你想调整某个字段的值并且不希望比它更小的字段的值发生变化,很多情况下这是不可能的,因为更小字段的最大最小值可能已经改变了。
    如GregorianCalendar最初被设置为1999年8月31日。调用add(Calendar.MONTH, 13),由于8+13=21>12造成了MONTH字段的值越界,所以MONTH值对12取模为9,同时更大的字段YEAR需要增加为2000,由由于9月没有31日,所以将DAY_OF_MONTH调整为最接近的30,所以最终的时间为2000年9月30日。
    注意:add(f, delta)方法会立即重新计算calendar的milliseconds值和所有字段。
  • roll(f, delta)方法同样可以给f字段的值设置delta偏移,但不会对更大的字段产生影响。相当于“调用后更大字段的值不会发生改变”规则的add(f, delta)方法调用。
    注意:使用add(f, delta)roll(f, delta)方法时一定要谨慎,如1999年1月31日当用户按下一个月的按钮时最好的结果是1999年2月28日,如果继续按下一个月按钮,那显示1999年3月31日还是1999年3月28日。

Calendar类的一些字段:

  • JANUARY=0, FEBRUARY=1, ……UNDECIMBER=12。各个月份(MONTH)常量为int类型,从0开始,UNDECIMBER在GregorianCalendar不会用到,但中国农历日历会用到(因为可能会多闰一个月,即一年13个月)。
  • SUNDAY=1, MONDAY=2, SATURDAY=7。各个星期(DAY_OF_WEEK)常量为int类型,从1开始。
  • ERA:纪年。如AD、BC
  • YEAR:年。如1970
  • MONTH:月。如1(FEBRUARY)
  • WEEK_OF_YEAR:当前年的周数。由getFirstDayOfWeek()getMinimalDaysInFirstWeek()决定。
  • WEEK_OF_MONTH:当前月的周数。由getFirstDayOfWeek()getMinimalDaysInFirstWeek()决定。
  • DATE:日。与DAY_OF_MONTH同义,指当月多少日/多少号。
  • DAY_OF_MONTH:日。与DATE同义。
  • DAY_OF_YEAR:当前年的总天数。
  • DAY_OF_WEEK:周几。取值为SUNDAY - SATURDAY。
  • DAY_OF_WEEK_IN_MONTH:当前月的第几周。与DAY_OF_WEEK字段一起使用时,就可以唯一地指定某月中的某一天。不会受getFirstDayOfWeek()getMinimalDaysInFirstWeek()影响。DAY_OF_MONTH 1-7总会对应DAY_OF_WEEK_IN_MONTH 1,也就是说每个月的1-7号总是当前月的第一周,2-8号是第二周,以此类推。DAY_OF_MONTH 0表示第一周的前一周,而负值表示当前月的倒数第几周,由于对齐方式和正向不一样,所以一个月的最后一个星期天被指定为DAY_OF_WEEK=SUNDAY, DAY_OF_WEEK_IN_MONTH=-1。
  • AM_PM:上午/下午。判断HOUR是指上午(AM)还是下午(PM)。
  • HOUR:时。12小时制的时。
  • HOUR_OF_DAY:时。24小时制的时。
  • MINUTE:分。
  • SECOND:秒。
  • MILLISECOND:毫秒。
  • ZONE_OFFSET:非夏令时时区偏移的毫秒数。相当于TimeZone#getRawOffset()
  • DST_OFFSET:夏令时时区偏移的毫秒数。相当于TimeZone#getDSTSavings()
  • FIELD_COUNT:Calendar的总字段个数。

Calendar的一些方法:

  • public boolean after(Object calendar) 比较两个Calendar所持有的Date对象,不依赖Calendar时区。
  • public boolean before(Object calendar) 比较两个Calendar所持有的Date对象,不依赖Calendar时区。
  • public final void clear() 清除所有时间字段,标记为reset并清零。
  • public final void clear(int field) 清除某个字段,标记为reset并清零。
  • public long getTimeInMillis() 返回Calendar所表示的时间毫秒数(1970-01-01T00:00:00Z所经过的毫秒数),如果必要会重新计算时间。
  • public final Date getTime()getTimeInMillis()返回的时间包装成Date对象返回。
  • public final void set(int year, int month, int day)系列方法 其中month从0开始算起,所以month最好使用Calendar.JANUARY、Calendar.FEBRUARY等常量。
  • public final void setTime(Date date)
  • public void setTimeInMillis(long milliseconds)

java.text.DateFormat

用来格式化/解析 日期和时间。DateFormat是一个抽象类,最常用的是它的子类SimpleDateFormat
可以根据想要格式化/解析的时Date还是Time还是DateTime的使用getDateInstance()getDateInstance(int style)getDateInstance(int style, Locale locale)getTimeInstance()getTimeInstance(int style)getTimeInstance(int style, Locale locale)getDateTimeInstance()getDateTimeInstance(int dateStyle, int timeStyle)getDateTimeInstance(int dateStyle, int timeStyle, Locale locale)不同的静态方法获取不同的DateFormat实例。
public final StringBuffer format(Object object, StringBuffer buffer, FieldPosition field)方法,可以将Date/Number日期类型的数据根据指定模式串格式化成想要显示的字符串。其中第一个参数必须是DateNumber对象。
public Date parse(String string)方法可以利用指定规则将字符串解析成Date对象。

java.text.SimpleDateFormat

以本地化敏感的方式格式化/解析时间数据,可以使用formatDate转成String,也可以使用parseString转成Date

Symbol Meaning Kind Example
{@code D} day in year (Number) 189
{@code E} day of week (Text) {@code E}/{@code EE}/{
{@code F} day of week in month (Number) 2 (2nd Wed in July)
{@code G} era designator (Text) AD
{@code H} hour in day (0-23) (Number) 0
{@code K} hour in am/pm (0-11) (Number) 0
{@code L} stand-alone month (Text) {@code L}:1 {@code LL}
{@code M} month in year (Text) {@code M}:1 {@code MM}
{@code S} fractional seconds (Number) 978
{@code W} week in month (Number) 2
{@code Z} time zone (RFC 822) (Time Zone) {@code Z}/{@code ZZ}/{
{@code a} am/pm marker (Text) PM
{@code c} stand-alone day of week (Text) {@code c}/{@code cc}/{
{@code d} day in month (Number) 10
{@code h} hour in am/pm (1-12) (Number) 12
{@code k} hour in day (1-24) (Number) 24
{@code m} minute in hour (Number) 30
{@code s} second in minute (Number) 55
{@code w} week in year (Number) 27
{@code y} year (Number) {@code yy}:10 {@code y
{@code z} time zone (Time Zone) {@code z}/{@code zz}/{
{@code ‘} escape for text (Delimiter) {@code ‘Date=’}:Date=
作者:shangmingchao 发表于2016/8/25 12:14:17 原文链接
阅读:40 评论:0 查看评论

Android简易实战教程--第二十二话《自定义组合控件模拟qq登录下拉框和其中的一些”小技巧”》

$
0
0

转载此文章请注明出处:点击打开链接   http://blog.csdn.net/qq_32059827/article/details/52313516

首先,很荣幸此专栏能被CSDN推荐到主页。荣幸的同时,也激励自己会把这个专栏一直更新下去。

进入今天的主题:

我们在qq登录的时候,会有一个下拉的按钮,来查看历史登录账号。这一篇就模拟这个效果,自定义组合框实现之。

这里面会用到popupwindow,对于popupwindow的原始用法欢迎看之前的一篇文章,对弹出窗体做过介绍:点击打开链接

今天不再使用那种方式来定义了,使用其他的api,实现方式更简单。同时,有一些”小技巧”在里面。

定义主布局:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:gravity="center_horizontal"
    android:layout_height="match_parent">

    <EditText
        android:id="@+id/et_number"
        android:layout_width="200dip"
        android:layout_height="wrap_content" />

    <ImageButton
        android:id="@+id/ib_down_arrow"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@id/et_number"
        android:layout_alignRight="@id/et_number"
        android:layout_alignTop="@id/et_number"
        android:layout_marginRight="5dip"
        android:background="@drawable/down_arrow" />

</RelativeLayout>

小技巧:图片的点击事件ImageButton能更好的胜任这个工作,但是使用它的时候,需要把背景去掉(ImageButton默认是携带一个背景的)。设置背景无的方式有两种:1、加入两个属性: android:background="@android:color/transparent" 作用是让背景为透明状态              android:src="@drawable/icon_home"   ;  2直接设置android:background="@drawable/down_arrow" 。一般对ImageButton图片使用第二种。

弹出窗体,窗体需要一个布局。显然,要实现模拟qq下拉需要用listview显示,它的布局如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:descendantFocusability="blocksDescendants"
    android:padding="5dip" >
<!-- android:descendantFocusability="blocksDescendants"表示让后辈只在自己那一块位置响应事件,不要抢占父组件本身的事件 -->
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/user" />

    <TextView
        android:id="@+id/tv_listview_item_number"
        android:layout_width="0dip"
        android:layout_height="wrap_content"
        android:layout_marginLeft="5dip"
        android:layout_marginRight="5dip"
        android:layout_weight="1"
        android:gravity="center"
        android:text="默认的号码"
        android:textSize="18sp" />

    <ImageButton
        android:id="@+id/ib_listview_item_delete"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/delete" />

</LinearLayout>
小技巧:注意在根布局里面加入的android:descendantFocusability="blocksDescendants"。如果去掉的话,当在点击listview的item的时候,事件会被他上边的ImageButton抢占走。加入它的目的就是:让LiearLayout的子组件只在自己那一块位置响应事件,不要抢占父组件本身的事件。

接下来,就是业务逻辑,如下:

package com.itydl.popupwindowdemo;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.graphics.drawable.BitmapDrawable;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.PopupWindow;
import android.widget.TextView;

public class MainActivity extends Activity implements OnClickListener, OnItemClickListener {

    private List<String> numberList; // 号码集合
	private ListView mListView;
	private EditText etNumber;
	private PopupWindow popupWindow;
	private NumberAdapter mAdapter;

	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        etNumber = (EditText) findViewById(R.id.et_number);
        ImageButton ibArrow = (ImageButton) findViewById(R.id.ib_down_arrow);
        
        ibArrow.setOnClickListener(this);
    }

	@Override
	public void onClick(View v) {
		showPopupWindow();
	}

	/**
	 * 弹出一个下拉窗体
	 */
	private void showPopupWindow() {
		initListView();
		
		// 创建PopupWindow对象,指定内容为ListView,宽度为输入框的宽度,高度为200,当前窗体可以获取焦点
		popupWindow = new PopupWindow(mListView, etNumber.getWidth() - 8, 200, true);

                /***************************************  这里需要添加两行代码   ***************************************************************/
		
                /**
		 * View anchor, 相对父组件(这里的父组件是一个编辑框)
		 * int xoff, x偏移量
		 * int yoff,y偏移量
		 * 说明:弹出窗体右上角与父组件左下角作为相对位置。
		 */
		popupWindow.showAsDropDown(etNumber, 4, -4);//表示mListView的窗体,相对父组件左移4个像素,上移4个像素
	}

	/**
	 * 初始化ListView
	 */
	private void initListView() {
		mListView = new ListView(this);
		/****/
		mListView.setOnItemClickListener(this);
		
		numberList = new ArrayList<String>();
		for (int i = 0; i < 30; i++) {
			//数据源添加数据
			numberList.add("10000060" + i);
		}
		
		mAdapter = new NumberAdapter();
		mListView.setAdapter(mAdapter);
	}
	
	class NumberAdapter extends BaseAdapter {

		@Override
		public int getCount() {
			return numberList.size();
		}

		@Override
		public View getView(final int position, View convertView, ViewGroup parent) {
			if(convertView == null) {
				convertView = View.inflate(MainActivity.this, R.layout.listview_item, null);
			}
			
			TextView tvNumber = (TextView) convertView.findViewById(R.id.tv_listview_item_number);
			ImageButton ibDelete = (ImageButton) convertView.findViewById(R.id.ib_listview_item_delete);
			tvNumber.setText(numberList.get(position));
			
			ibDelete.setOnClickListener(new OnClickListener() {
				
				@Override
				public void onClick(View v) {
					numberList.remove(position);//移除数据源位置
					mAdapter.notifyDataSetChanged();//通知listview刷新
				}
			});
			
			return convertView;
		}

		@Override
		public Object getItem(int position) {
			// TODO Auto-generated method stub
			return null;
		}

		@Override
		public long getItemId(int position) {
			// TODO Auto-generated method stub
			return 0;
		}

		
	}

	@Override
	public void onItemClick(AdapterView<?> parent, View view, int position,
			long id) {
//		System.out.println("onItemClick");
		//获取点击那项的数据
		String number = numberList.get(position);
		etNumber.setText(number);
		popupWindow.dismiss(); //关闭窗体
	}
}
可以运行程序看看效果了:

基本实现了需求,但是最后还有几个小技巧要说一下:

1、您会发现等弹出窗体后,没办法关闭窗体,按返回键都关闭不掉,这个时候就需要设置两行代码解决这个问题。需要在哪里加入我已经在代码中标注出来了,在那个地方加入两行代码:

popupWindow.setOutsideTouchable(true);//popupWindow可以接收其它位置的点击事件触发
        popupWindow.setBackgroundDrawable(new BitmapDrawable());/

2、如果想干掉listview旁边的下拉滚动条,只需要加入下边一行代码即可:

mListView.setVerticalScrollBarEnabled(false);

到此,小demo完毕了。

欢迎关注本博客点击打开链接  http://blog.csdn.net/qq_32059827,每天花上5分钟,阅读一篇有趣的安卓小文哦。


作者:qq_32059827 发表于2016/8/25 13:11:45 原文链接
阅读:153 评论:0 查看评论

Unity3D —— protobuf网络框架

$
0
0

前言:

        protobuf是google的一个开源项目,主要的用途是:

1.数据存储(序列化和反序列化),这个功能类似xml和json等;

2.制作网络通信协议;


一、资源下载:

1.github源码地址:https://github.com/mgravell/protobuf-net

2.google项目源码下载地址(访问需翻墙):https://code.google.com/p/protobuf-net/


二、数据存储:

        C#语言方式的导表和解析过程,在之前的篇章中已经有详细的阐述:Unity —— protobuf 导excel表格数据,建议在看后续的操作之前先看一下这篇文档,因为后面设计到得一些操作与导表中是一致的,而且在理解了导表过程之后,能够快速地理解协议数据序列化反序列化的过程。


三、网络协议:

1.设计思想:

        有两个必要的数据:协议号协议类型,将这两个数据分别存储起来

  • 当客户端向服务器发送数据时,会根据协议类型加上协议号,然后使用protobuf序列化之后再发送给服务器;
  • 当服务器发送数据给客户端时,根据协议号,用protobuf根据协议类型反序列化数据,并调用相应回调方法。

        由于数据在传输过程中,都是以数据流的形式存在的,而进行解析时无法单从protobuf数据中得知使用哪个解析类进行数据反序列化,这就要求我们在传输protobuf数据的同时,携带一个协议号,通过协议号和协议类型(解析类)之间的对应关系来确定进行数据反序列化的解析类。

       

        此处协议号的作用就是用来确定用于解析数据的解析类,所以也可能称之为协议类型名,可以是stringint类型的数据。


2.特点分析:

        使用protobuf作为网络通信的数据载体,具有几个优点:

  • 通过序列化之后数据量比较小
  • 而且以key-value的方式存储数据,这对于消息的版本兼容比较强;
  • 此外,由于protobuf提供的多语言支持,所以使用protobuf作为数据载体定制的网络协议具有很强的跨语言特性

四、样例实现:

1.协议定义:

        在之前导表的时候,我们得到了.proto的解析类,这是protobuf提供的一种特殊的脚本,具有格式简单、可读性强和方便拓展的特点,所以接下来我们就是使用proto脚本来定义我们的协议。例如:

// 物品
message Item
{
    required int32 Type 	= 1;	//游戏物品大类
    optional int32 SubType 	= 2;	//游戏物品小类
    required int32 num 		= 3;	//游戏物品数量
}

// 物品列表
message ItemList
{
    repeated Item item 	= 1;	//物品列表
}
        上述例子中,Item相当于定义了一个数据结构或者是类,而ItemList是一个列表,列表中的每个元素都是一个Item对象。注意结构关键词:

  • required:必有的属性
  • optional:可选属性
  • repeated:数组
        其实protobuf在这里只是提供了一个数据载体,通过在.proto中定义数据结构之后,需要使用与导表时一样的操作,步骤为:

  • 使用protoc.exe将.proto文件转化为.protodesc中间格式;
  • 使用protogen.exe将中间格式为.protodesc生成指定的高级语言类,我们在Unity中使用的是C#,所以结果是.cs类
        经过上述步骤之后,我们得到了协议类型对应的C#反序列化类,当我们收到服务器数据时,根据协议号找到协议类型,从而使用对应的反序列化的类对数据进行反序列化,得到最终的服务器数据内容。

        在这里,我们以登录为例,首先要清楚登录需要几个数据,正常情况下至少包含两个数据,即账号和密码,都是字符串类型,即定义cs_login.proto协议脚本,内容如下:
package cs;

message CSLoginInfo
{
	required string UserName = 1;//账号
	required string Password = 2;//密码
}

//发送登录请求
message CSLoginReq
{
	required CSLoginInfo LoginInfo = 1; 
}
//登录请求回包数据
message CSLoginRes
{
	required uint32 result_code = 1; 
}
        package关键字后面的名称为.proto转为.cs之后的命名空间namespace的值,用message可以定义类,这里定义了一个CSLoginInfo的数据类,该类包含了账号和密码两个字符串类型的属性。然后定义了两个消息结构:
  • CSLoginReq登录请求消息,携带的数据是一个CSLoginInfo类型的对象数据;
  • CSLoginRes登录请求服务器返回的数据类型,返回结果是一个uint32无符号的整型数据,即结果码。
        上面定义的是协议类型,除此之外我们还需要为每一个协议类型定义一个协议号,这里可以用一个枚举脚本cs_enum.proto来保存,脚本内容为:
package cs;

enum EnmCmdID
{
	CS_LOGIN_REQ = 10001;//登录请求协议号
	CS_LOGIN_RES = 10002;//登录请求回包协议号
}
        使用protoc.exe和protogen.exe将这两个protobuf脚本得到C#类,具体步骤参考导表使用的操作,这里我直接给出自动化导表使用的批处理文件general_all.bat内容,具体文件目录可以根据自己放置情况进行调整:
::---------------------------------------------------
::第二步:把proto翻译成protodesc
::---------------------------------------------------
call proto2cs\protoc protos\cs_login.proto --descriptor_set_out=cs_login.protodesc
call proto2cs\protoc protos\cs_enum.proto --descriptor_set_out=cs_enum.protodesc
::---------------------------------------------------
::第二步:把protodesc翻译成cs
::---------------------------------------------------
call proto2cs\ProtoGen\protogen -i:cs_login.protodesc -o:cs_login.cs
call proto2cs\ProtoGen\protogen -i:cs_enum.protodesc -o:cs_enum.cs
::---------------------------------------------------
::第二步:把protodesc文件删除
::---------------------------------------------------
del *.protodesc

pause
        转换结束后,我们的得到了两个.cs文件分别是:cs_enum.cs和cs_login.cs,将其放入到我们的Unity项目中,以便于接下来序列化和反序列化数据的使用。


2.协议数据构建:

        直接在项目代码中通过using cs引入协议解析类的命名空间,然后创建消息对象,并对对象的属性进行赋值,即可得到协议数据对象,例如登录请求对象的创建如下:

        CSLoginInfo mLoginInfo = new CSLoginInfo();
        mLoginInfo.UserName = "linshuhe";
        mLoginInfo.Password = "123456";
        CSLoginReq mReq = new CSLoginReq();
        mReq.LoginInfo = mLoginInfo;
        从上述代码,可以得到登录请求对象mReq,里面包含了一个CSLoginInfo对象mLoginInfo,再次枚举对象中找到与此协议类型对应的协议号,即:EnmCmdID.CS_LOGIN_REQ


3.数据的序列化和反序列化:

        数据发送的时候必须以数据流的形式进行,所以这里我们需要考虑如何将要发送的protobuf对象数据进行序列化,转化为byte[]字节数组,这就需要借助ProtoBuf库为我们提供的Serializer类的Serialize方法来完成,而反序列化则需借助Deserialize方法,将这两个方法封装到PackCodec类中:

using UnityEngine;
using System.Collections;
using System.IO;
using System;
using ProtoBuf;

/// <summary>
/// 网络协议数据打包和解包类
/// </summary>
public class PackCodec{
    /// <summary>
    /// 序列化
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="msg"></param>
    /// <returns></returns>
    static public byte[] Serialize<T>(T msg)
    {
        byte[] result = null;
        if (msg != null)
        {
            using (var stream = new MemoryStream())
            {
                Serializer.Serialize<T>(stream, msg);
                result = stream.ToArray();
            }
        }
        return result;
    }

    /// <summary>
    /// 反序列化
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="message"></param>
    /// <returns></returns>
    static public T Deserialize<T>(byte[] message)
    {
        T result = default(T);
        if (message != null)
        {
            using (var stream = new MemoryStream(message))
            {
                result = Serializer.Deserialize<T>(stream);
            }
        }
        return result;
    }
}
        使用方法很简单,直接传入一个数据对象即可得到字节数组:

        byte[] buf = PackCodec.Serialize(mReq);

        为了检验打包和解包是否匹配,我们可以直接做一次本地测试:将打包后的数据直接解包,看看数据是否与原来的一致:

using UnityEngine;
using System.Collections;
using System;
using cs;
using ProtoBuf;
using System.IO;

public class TestProtoNet : MonoBehaviour {

	// Use this for initialization
	void Start () {
        CSLoginInfo mLoginInfo = new CSLoginInfo();
        mLoginInfo.UserName = "linshuhe";
        mLoginInfo.Password = "123456";
        CSLoginReq mReq = new CSLoginReq();
        mReq.LoginInfo = mLoginInfo;

        byte[] pbdata = PackCodec.Serialize(mReq);
        CSLoginReq pReq = PackCodec.Deserialize<CSLoginReq>(pbdata);
        Debug.Log("UserName = " + pReq.LoginInfo.UserName + ", Password = " + pReq.LoginInfo.Password);
	}

    // Update is called once per frame
    void Update () {
	
	}
}

        将此脚本绑到场景中的相机上,运行得到以下结果,则说明打包和解包完全匹配:
        


4.数据发送和接收:

        这里我们使用的网络通信方式是Socket的强联网方式,关于如何在Unity中使用Socket进行通信,可以参考我之前的文章:Unity —— Socket通信(C#),Unity客户端需要复制此项目的ClientSocket.csByteBuffer.cs两个类到当前项目中。

        此外,服务器可以参照之前的方式搭建,唯一不同的是RecieveMessage(object clientSocket)方法解析数据的过程需要进行修改,因为需要使用protobuf-net.dll进行数据解包,所以需要参考客户端的做法,把protobuf-net.dll复制到服务器项目中的Protobuf_net目录下:

        
        假如由于直接使用源码而不用.dll会出现不安全保存,需要在Visual Studio中设置允许不安全代码,具体步骤为:在“解决方案”中选中工程,右键“数据”,选择“生成”页签,勾选“允许不安全代码”:

         

          当然,解析数据所用的解析类和协议号两个脚本cs_login.cs和cs_enum.cs也应该添加到服务器项目中,保证客户端和服务器一直,此外PackCodec.cs也需要添加到服务器代码中但是要把其中的using UnityEngine给去掉防止报错,最终服务器目录结构如下:

         


5.完整协议数据的封装:

        从之前说过的设计思路分析,我们在发送数据的时候除了要发送关键的protobuf数据之外,还需要带上两个附件的数据:协议头(用于进行通信检验)和协议号(用于确定解析类)。假设我们的是:

        协议头:用于表示后面数据的长度一个short类型的数据:

        /// <summary>
        /// 数据转换,网络发送需要两部分数据,一是数据长度,二是主体数据
        /// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        private static byte[] WriteMessage(byte[] message)
        {
            MemoryStream ms = null;
            using (ms = new MemoryStream())
            {
                ms.Position = 0;
                BinaryWriter writer = new BinaryWriter(ms);
                ushort msglen = (ushort)message.Length;
                writer.Write(msglen);
                writer.Write(message);
                writer.Flush();
                return ms.ToArray();
            }
        }

        协议号:用于对应解析类,这里我们使用的是int类型的数据:

        private byte[] CreateData(int typeId,IExtensible pbuf)
    {
        byte[] pbdata = PackCodec.Serialize(pbuf);
        ByteBuffer buff = new ByteBuffer();
        buff.WriteInt(typeId);
        buff.WriteBytes(pbdata);
        return buff.ToBytes();
    }
        客户端发送登录数据时测试脚本TestProtoNet如下,测试需要将此脚本绑定到当前场景的相机上:

using UnityEngine;
using System.Collections;
using System;
using cs;
using Net;
using ProtoBuf;
using System.IO;

public class TestProtoNet : MonoBehaviour {

	// Use this for initialization
	void Start () {


        CSLoginInfo mLoginInfo = new CSLoginInfo();
        mLoginInfo.UserName = "linshuhe";
        mLoginInfo.Password = "123456";
        CSLoginReq mReq = new CSLoginReq();
        mReq.LoginInfo = mLoginInfo;

        byte[] data = CreateData((int)EnmCmdID.CS_LOGIN_REQ, mReq);
        ClientSocket mSocket = new ClientSocket();
        mSocket.ConnectServer("127.0.0.1", 8088);
        mSocket.SendMessage(data);
    }

    private byte[] CreateData(int typeId,IExtensible pbuf)
    {
        byte[] pbdata = PackCodec.Serialize(pbuf);
        ByteBuffer buff = new ByteBuffer();
        buff.WriteInt(typeId);
        buff.WriteBytes(pbdata);
        return WriteMessage(buff.ToBytes());
    }

    /// <summary>
    /// 数据转换,网络发送需要两部分数据,一是数据长度,二是主体数据
    /// </summary>
    /// <param name="message"></param>
    /// <returns></returns>
    private static byte[] WriteMessage(byte[] message)
    {
        MemoryStream ms = null;
        using (ms = new MemoryStream())
        {
            ms.Position = 0;
            BinaryWriter writer = new BinaryWriter(ms);
            ushort msglen = (ushort)message.Length;
            writer.Write(msglen);
            writer.Write(message);
            writer.Flush();
            return ms.ToArray();
        }
    }

    // Update is called once per frame
    void Update () {
	
	}
}
        服务器接受数据解包过程参考打包数据的格式,在RecieveMessage(object clientSocket)中,解析数据的核心代码如下:

        ByteBuffer buff = new ByteBuffer(result);
        int datalength = buff.ReadShort();
        int typeId = buff.ReadInt();
        byte[] pbdata = buff.ReadBytes();
        //通过协议号判断选择的解析类
        if(typeId == (int)EnmCmdID.CS_LOGIN_REQ)
        {
                CSLoginReq clientReq = PackCodec.Deserialize<CSLoginReq>(pbdata);
                string user_name = clientReq.LoginInfo.UserName;
                string pass_word = clientReq.LoginInfo.Password;
                Console.WriteLine("数据内容:UserName={0},Password={1}", user_name, pass_word);
                }
        }
        上面通过typeId来找到匹配的数据解析类,协议少的时候可以使用这种简单的使用if语句分支判断来实现,但是假如协议类型多了,则需要进一步封装查找方法,常用的方法有:定义一个Dictionary<int,Type>字典来存放协议号(int)和协议类型(Type)的对应关系。


6.运行结果:

        启动服务器,然后运行Unity中的客户端,得到正确的结果应该如下:

        

        项目服务器和客户端的完整代码可以前往此处下载:protobuf-net网络协议的定制


作者:linshuhe1 发表于2016/8/25 19:44:40 原文链接
阅读:53 评论:0 查看评论

android--UI--导航条toolbar

$
0
0

android–UI–导航条toolbar

标签(空格分隔): android UI toolbar

参考http://www.codeceo.com/article/android-toolbar-develop.html,自己加以理解。

开发工具:
AS2.1.2

关于toolbar

Google在2015的IO大会上发布了系列的Material Design风格的控件。
其中ToolBar是替代ActionBar的控件。
由于ActionBar在各个安卓版本和定制Rom中的效果表现不一,导致严重的碎片化问题
ToolBar应运而生。

使用toolbar

效果图:

这里写图片描述

关于引入兼容包问题

我直接打开的as新建工程,设置最低版本4.0,自带sdk6.0,没有这个问题。

添加toolbar

activity—_main.xml文件添加引入

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.administrator.toolbartest.MainActivity"
    android:orientation="vertical">

    <!--引入Toolbar代码,包含一个TextView-->
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#98bc98" >

        <!--自定义控件-->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Clock" />
    </android.support.v7.widget.Toolbar>



    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />
</LinearLayout>

引入Toolbar代码,包含一个TextView

ToolbarActivity 中调用 Toolbar

导入包 import android.support.v7.widget.Toolbar;
调用Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
使用set方式设置Toolbar的各种属性,只要toolbar.set然后ide就会告诉你能设置什么。
具体看代码

import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.os.Bundle;
import android.view.Window;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE);// 去掉了默认的导航栏(注意和以前的不同)
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        toolbar.setNavigationIcon(R.mipmap.ic_home);//设置导航栏图标
        toolbar.setLogo(R.mipmap.ic_launcher);//设置app logo
        toolbar.setTitle("主标题");//设置主标题
        toolbar.setSubtitle("子标题");//设置子标题


    }
}

效果如图:
这里写图片描述

创建menu文件


原图中还有右边的搜索提醒等等是一个menu控件
和eclipse不同,as创建menu文件夹也需要创建文件夹。具体如下:
参考文章:http://www.cnblogs.com/ssqqhh/p/5213331.html

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/action_search"
        android:icon="@mipmap/ic_search"
        android:title="@string/menu_search"
        app:showAsAction="ifRoom" />

    <item
        android:id="@+id/action_notification"
        android:icon="@mipmap/ic_notifications"
        android:title="@string/menu_notifications"
        app:showAsAction="ifRoom" />

    <item
        android:id="@+id/action_item1"
        android:title="@string/item_01"
        app:showAsAction="never" />

    <item
        android:id="@+id/action_item2"
        android:title="@string/item_02"
        app:showAsAction="never" />
</menu>

代码中绑定
toolbar.inflateMenu(R.menu.base_toolbar_menu);//设置右上角的填充菜单
这里写图片描述

所有的控件添加点击事件

样式设置好了最重要的就是点击事件。
各个部分的点击事件:参考文章 http://blog.csdn.net/flykozhang/article/details/50280109
此处输入图片的描述

toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this,"Navigation",Toast.LENGTH_SHORT).show();
            }
        });
toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
            @Override
            public boolean onMenuItemClick(MenuItem item) {
                int menuItemId = item.getItemId();
                if (menuItemId == R.id.action_search) {
                    Toast.makeText(ToolBarActivity.this , R.string.menu_search , Toast.LENGTH_SHORT).show();

                } else if (menuItemId == R.id.action_notification) {
                    Toast.makeText(ToolBarActivity.this , R.string.menu_notifications , Toast.LENGTH_SHORT).show();

                } else if (menuItemId == R.id.action_item1) {
                    Toast.makeText(ToolBarActivity.this , R.string.item_01 , Toast.LENGTH_SHORT).show();

                } else if (menuItemId == R.id.action_item2) {
                    Toast.makeText(ToolBarActivity.this , R.string.item_02 , Toast.LENGTH_SHORT).show();

                }
                return true;
            }
        });

其他点击事件用的不多

总体代码:

import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.os.Bundle;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE);// 去掉了默认的导航栏(注意和以前的不同)
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        toolbar.setNavigationIcon(R.mipmap.ic_home);//设置导航栏图标
        toolbar.setLogo(R.mipmap.ic_launcher);//设置app logo
        toolbar.setTitle("主标题");//设置主标题
        toolbar.setSubtitle("子标题");//设置子标题
        toolbar.inflateMenu(R.menu.base_toolbar_menu);//设置右上角的填充菜单
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this,"Navigation",Toast.LENGTH_SHORT).show();
            }
        });
        toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
            @Override
            public boolean onMenuItemClick(MenuItem item) {
                int menuItemId = item.getItemId();
                if (menuItemId == R.id.action_search) {
                    Toast.makeText(MainActivity.this , "menu_search" , Toast.LENGTH_SHORT).show();

                } else if (menuItemId == R.id.action_notification) {
                    Toast.makeText(MainActivity.this , "menu_notifications" , Toast.LENGTH_SHORT).show();

                } else if (menuItemId == R.id.action_item1) {
                    Toast.makeText(MainActivity.this , "item_01" , Toast.LENGTH_SHORT).show();

                } else if (menuItemId == R.id.action_item2) {
                    Toast.makeText(MainActivity.this , "item_02" , Toast.LENGTH_SHORT).show();

                }
                return false;
            }
        });
    }


}

效果

这里写图片描述
这里写图片描述
这里写图片描述

作者:lw_zhaoritian 发表于2016/8/25 19:45:15 原文链接
阅读:62 评论:0 查看评论

ViewPager 实现 Galler 效果, 显示中间大图显示,两边小图展示

$
0
0
正常情况下, ViewPager 一页只能显示一项数据, 但是我们常常看到网上,特别是电视机顶盒的首页经常出现中间大图显示两端也都露出一点来,这种效果怎么实现呢?先上一张效果图:


大家第一眼肯定想到了Gallery,这是最早android图库自带的效果,现在基本不用,那有没有其他好的办法呢?我们首先考虑的还是ViewPager+PagerAdapter的实现策略。
后面在网上了搜了一下, 发现要实现上面的效果,我们需要注意两个方面,首先是怎么在两边显示两个小图,第二,怎么实现无限滑动。
1,首先就是用到了View的android:clipChildren属性,.简单来说父View是默认是束缚子View 的显示范围的,所以当我们在父View有 padding , 那么 子View 则在 padding区域是不能显示内容的。当设置android:clipChildren="false"的时候,子View 就可以在父View 的padding内容区域显示内容了。
2,实现无限循环很简单,网上也有很多的解决方案,我这里不考虑性能上的东西,且看下面简单的代码:
private class ImageAdapter extends PagerAdapter{  
           
        private ArrayList<String> viewlist;  
   
        public ImageAdapter(ArrayList<String> viewlist) {  
            this.viewlist = viewlist;  
        }  
   
        @Override  
        public int getCount() {  
            //设置成最大,使用户看不到边界,大家可以去查询下这个大小  
            return Integer.MAX_VALUE;  
        }       
         @Override    
         public void destroyItem(ViewGroup container, int position,    
                 Object object) {    
             //注:不要在这里调用removeView  
         }   
		 
         @Override    
         public Object instantiateItem(ViewGroup container, int position) {  
             //对ViewPager页号求模取出View列表中要显示的项  
             position %= viewlist.size();  
             if (position<0){  
                 position = viewlist.size()+position;  
             }  
			 
			 //这里是view
            ViewHolder viewHolder = null;
            View view = LayoutInflater.from(mContext).inflate(
                R.layout.item_finefare_layout, null);
          if (viewHolder == null) {
            viewHolder = new ViewHolder(view);
           }
            bindView(viewHolder, data);

          container.addView(view, LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT);
             return view;    
         }    
    }  

上面代码应该注意的几点:

  • getCount() 方法的返回值:这个值直接关系到ViewPager的“边界”,因此当我们把它设置为Integer.MAX_VALUE之后,用户基本就看不到这个边界了(估计滑到这里的时候电池已经挂了吧o_O)。当然,通常情况下设置为100倍实际内容个数也是可以的,之前看的某个实现就是这么干的。

  • instantiateItem() 方法position的处理:由于我们设置了count为 Integer.MAX_VALUE,因此这个position的取值范围很大很大,但我们实际要显示的内容肯定没这么多(往往只有几项),所以这里肯定会有求模操作。但是,简单的求模会出现问题:考虑用户向左滑的情形,则position可能会出现负值。所以我们需要对负值再处理一次,使其落在正确的区间内。

  • instantiateItem() 方法父组件的处理:通常我们会直接addView,但这里如果直接这样写,则会抛出IllegalStateException假设一共有三个view,则当用户滑到第四个的时候就会触发这个异常,原因是我们试图把一个有父组件的View添加到另一个组件

经过上面的解释,我们已经很清楚了,以下是代码的详细实现,数据来源于网上,大家可以自行模拟:
ViewPager类:
public class WelfareAdapter extends PagerAdapter {

    private Context mContext;
    private List<PanicBean> dataList = new ArrayList<>();

    public WelfareAdapter(Context mContext) {
        this.mContext = mContext;
    }

    public void setDatas(List<PanicBean> list) {
        if (list.size() <= 0) {
            dataList.clear();
            notifyDataSetChanged();
            return;
        }
        dataList.clear();
        dataList.addAll(list);
        notifyDataSetChanged();
    }

    @Override
    public int getCount() {
        return Integer.MAX_VALUE;
    }

    @Override
    public int getItemPosition(Object object) {
        return POSITION_NONE;
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        position %= dataList.size();
        if (position<0){
            position = dataList.size()+position;
        }

        PanicBean data = dataList.get(position);

        ViewHolder viewHolder = null;
        View view = LayoutInflater.from(mContext).inflate(
                R.layout.item_finefare_layout, null);
        if (viewHolder == null) {
            viewHolder = new ViewHolder(view);
        }
        bindView(viewHolder, data);

        container.addView(view, LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT);
        return view;
    }

    private void bindView(ViewHolder viewholder, final PanicBean data) {
        Glide.with(mContext).load(data.pic).into(viewholder.welfareImage);

        viewholder.welfareImage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ToastUtils.showToast("你点击了"+data.href);
            }
        });
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view == object;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
//        container.removeView((View) object);
    }

    class ViewHolder {
        @BindView(R.id.welfare_image)
        RoundedImageView welfareImage;

        ViewHolder(View view) {
            ButterKnife.bind(this, view);
            view.setTag(this);
        }

        public void reset() {
            welfareImage.setBackground(mContext.getResources().getDrawable(R.drawable.welfare_default_icon));
        }
    }

}
用到的布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:ptr="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center">

    <com.yju.app.widght.image.RoundedImageView
        android:id="@+id/welfare_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center"
        android:scaleType="fitXY"
        ptr:corner_radius="3dp"
        android:src="@drawable/welfare_default_icon" />

</LinearLayout>

用到的数据Bean(这个大家根据情况自行模拟,只要是个列表就行)
public class FineFareEntity {

    public List<PanicBean> panic;

    public static class PanicBean {
        public String id;
        public long endtime;
        public String pic;
        public int type;
        public String href;
        public String title;
        public String share;
    }
}

为了方便使用我们都自定义成View,方便以后代码维护:
public class WelfareView extends SimpleLinearLayout {

    @BindView(R.id.finefare_count)
    TextView finefareCount;
    @BindView(R.id.viewPager)
    ViewPager viewPager;
    @BindView(R.id.finefare_name)
    TextView finefareName;
    @BindView(R.id.welfare_view)
    LinearLayout welfareView;

    private WelfareAdapter adapter = null;
    private List<PanicBean> welfareList = null;

    public WelfareView(Context context) {
        super(context);
    }

    public WelfareView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void initViews() {
        contentView = inflate(mContext, R.layout.layout_welfare, this);
        ButterKnife.bind(this);

        initViewPager();

        initTouch();

    }

    private void initTouch() {
        //这里要把父类的touch事件传给子类,不然边上的会滑不动
        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return viewPager.dispatchTouchEvent(event);
            }
        });

        viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            }

            @Override
            public void onPageSelected(int position) {
                position %= welfareList.size();
                if (position<0){
                    position = welfareList.size()+position;
                }
                finefareName.setText(welfareList.get(position).id);
            }

            @Override
            public void onPageScrollStateChanged(int state) {

            }
        });

    }

    private void initViewPager() {
        viewPager.setOffscreenPageLimit(3);
        viewPager.setPageTransformer(true, new ScalePagerTransformer());
        //设置Pager之间的间距
        viewPager.setPageMargin(UIUtils.dp2px(mContext, 15));

        adapter = new WelfareAdapter(mContext);
        viewPager.setAdapter(adapter);
    }

    public void setWelfareData(List<PanicBean> datas) {
        this.welfareList = datas;
        welfareView.setVisibility(datas.size()>0?VISIBLE:GONE);
        finefareCount.setText("共有" + datas.size() + "个福利");
        finefareName.setText(welfareList.get(getCurrentDisplayItem()).title);

        adapter = new WelfareAdapter(mContext);
        adapter.setDatas(datas);
        viewPager.setAdapter(adapter);
        viewPager.setCurrentItem(adapter.getCount() > 0 ? 1 : 0, true);

    }

    public int getCurrentDisplayItem() {
        if (viewPager != null) {
            return viewPager.getCurrentItem();
        }
        return 0;
    }

}
这里有一个滑动缩放的类:
public class ScalePagerTransformer implements ViewPager.PageTransformer {

        private static final float MIN_SCALE = 0.85f;
        private static final float MIN_ALPHA = 0.5f;

        @Override
        public void transformPage(View view, float position) {
            if (position >= -1 || position <= 1) {
                final float height = view.getHeight();
                final float width = view.getWidth();
                final float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));
                final float vertMargin = height * (1 - scaleFactor) / 2;
                final float horzMargin = width * (1 - scaleFactor) / 2;

                view.setPivotY(0.5f * height);
                view.setPivotX(0.5f * width);

                if (position < 0) {
                    view.setTranslationX(horzMargin - vertMargin / 2);
                } else {
                    view.setTranslationX(-horzMargin + vertMargin / 2);
                }

                view.setScaleX(scaleFactor);
                view.setScaleY(scaleFactor);

                view.setAlpha(MIN_ALPHA + (scaleFactor - MIN_SCALE) / (1 - MIN_SCALE) * (1 - MIN_ALPHA));
            }
        }
    }


用到的布局(android:clipChildren="false"需要注意):
<span style="color:#333333;"><?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/welfare_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    </span><span style="color:#ff0000;">android:clipChildren="false"</span><span style="color:#333333;">
    android:layout_marginTop="10dp"
    android:background="@color/c12"
    android:orientation="vertical">


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <View
            style="@style/vertical_bold_line_c8"
            />

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:padding="10dp">

            <TextView
                style="@style/style_c6_s16"
                android:layout_centerVertical="true"
                android:text="精品福利" />

            <TextView
                android:id="@+id/finefare_count"
                style="@style/style_c6_s14"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:drawablePadding="3dp"
                android:drawableRight="@drawable/arrow"
                android:text="共有n个福利" />
        </RelativeLayout>
    </LinearLayout>

    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="140dp"
        android:layout_marginLeft="45dp"
        android:layout_marginRight="45dp"
         />

    <TextView
        android:id="@+id/finefare_name"
        style="@style/style_c8_s16"
        android:layout_gravity="center"
        android:layout_marginBottom="15dp"
        android:layout_marginTop="15dp"
        android:text="" />
</LinearLayout>
</span>

最后就是在我们的主代码中写个歌测试了,
 private void initWelfare() {
        String welfare = FileUtils.readAssert(getActivity(), "welfare.txt");
        FineFareEntity entity=JsonUtils.parseJson(welfare,FineFareEntity.class);
        if (entity!=null){
            welfareView.setWelfareData(entity.panic);
        }
    }
涉及到的布局:
<com.yju.app.shihui.welfare.view.WelfareView
                android:id="@+id/welfare_view"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:minHeight="160dp"
                />

这里的FileUtils.readAssert 代码:
public static String readAssert(Context context, String fileName){
        String resultString="";
        try {
            InputStream inputStream=context.getResources().getAssets().open(fileName);
            byte[] buffer=new byte[inputStream.available()];
            inputStream.read(buffer);
            resultString=new String(buffer,"utf-8");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return resultString;
    }
最后为了方便大家的模拟,我把数据给大家,有点多,大家自己放到assert目录下,
{
panic: [
{
id: "2412",
endtime: 1472097600000,
pic: "http://pc1.img.ymatou.com/G02/M04/E3/67/CgvUBVe9vyOAV47CAACXZZs5GrU558_o.jpg",
type: 1,
href: "http://evt.ymatou.com/n770",
title: "今日限时抢",
share: ""
},
{
id: "2417",
endtime: 1472097600000,
pic: "http://pc1.img.ymatou.com/G02/M09/E4/37/CgvUA1e91VmAMYrwAAC5qcblOUg650_o.jpg",
type: 1,
href: "http://evt.ymatou.com/n781",
title: "今日限时抢",
share: ""
},
{
id: "2413",
endtime: 1472097600000,
pic: "http://pc1.img.ymatou.com/G02/M05/E3/D4/CgvUA1e9v2SAVf3qAAB9GcBIWYA268_o.jpg",
type: 1,
href: "http://evt.ymatou.com/n771",
title: "今日限时抢",
share: ""
},
{
id: "2414",
endtime: 1472097600000,
pic: "http://pc1.img.ymatou.com/G02/M05/E3/69/CgvUBVe9v4aAMaFRAABWy73vn2g252_o.jpg",
type: 1,
href: "http://evt.ymatou.com/n772",
title: "今日限时抢",
share: ""
},
{
id: "2415",
endtime: 1472097600000,
pic: "http://pc1.img.ymatou.com/G02/M06/E3/02/CgvUBFe9v6WAP85NAAC6EK5e5Vg469_o.jpg",
type: 1,
href: "http://evt.ymatou.com/n773",
title: "今日限时抢",
share: ""
},
{
id: "2416",
endtime: 1472097600000,
pic: "http://pc1.img.ymatou.com/G02/M06/E3/02/CgvUBFe9v8CAHyXVAACELcKFT_M328_o.jpg",
type: 1,
href: "http://evt.ymatou.com/n775",
title: "今日限时抢",
share: ""
}
]
}

写得有点急,欢迎大家留言,有什么不懂得,请进我们的开发群,一定细心讲解:278792776





作者:xiangzhihong8 发表于2016/8/25 20:08:46 原文链接
阅读:91 评论:0 查看评论

Android之内存管理-内存监测-内存优化

$
0
0

推荐文章:Android进程与内存及内存泄漏

Android之内存管理

1.1 Dalvik
Dalvik虚拟机是Android程序的虚拟机,是Android中Java程序的运行基础。其指令集基于寄存器架构,执行其特有的文件格式——dex字节码来完成对象生命周期管理、堆栈管理、线程管理、安全异常管理、垃圾回收等重要功能。
Dalvik虚拟机的内存大体上可以分为 Java Object Heap、Bitmap Memory和Native Heap三种。
Java Object Heap:用于分配对象
Bitmap Memory:用来处理图像,≥Android 3.0, 归到Object Heap中
Native Heap: malloc分配,受系统限制

1.2 查看单个进程最大内存限制

Android设备出厂以后,java虚拟机对单个应用的最大内存分配就确定下来了,超出这个值就会OOM。

这个属性值是定义在/system/build.prop文件中的
dalvik.vm.heapstartsize=8m   //它表示堆分配的初始大小

它会影响到整个系统对RAM的使用程度,和第一次使用应用时的流畅程度。它值越小,系统ram消耗越慢,但一些较大应用一开始不够用,需要调用gc和堆调整策略,导致应用反应较慢。它值越大,这个值越大系统ram消耗越快,但是应用更流畅。

dalvik.vm.heapgrowthlimit=64m // 单个应用可用最大内存

主要对应的是这个值,它表示单个进程内存被限定在64m,即程序运行过程中实际只能使用64m内存,超出就会报OOM。(仅仅针对dalvik堆,不包括native堆)

dalvik.vm.heapsize=384m  //heapsize参数表示单个进程可用的最大内存,

但如果存在heapgrowthlimit参数,则以heapgrowthlimit为准.
heapsize表示不受控情况下的极限堆,表示单个虚拟机或单个进程可用的最大内存。而android上的应用是带有独立虚拟机的,也就是每开一个应用就会打开一个独立的虚拟机(这样设计就会在单个程序崩溃的情况下不会导致整个系统的崩溃)。
注意:在设置了heapgrowthlimit的情况下,单个进程可用最大内存为heapgrowthlimit值。在android开发中,如果要使用大堆,需要在manifest中指定android:largeHeap为true,这样dvm heap最大可达heapsize。

Android不同设备单个进程可用内存是不一样的,可以查看/system/build.prop文件。

# This is a high density device with more memory, so larger vm heaps for it.
dalvik.vm.heapsize=24m

上面heapsize参数表示单个进程可用的最大内存,单如果存在如下参数:

dalvik.vm.heapgrowthlimit=16m

largeheaplimit参数表示单个进程内存被限定在16m,即程序运行过程中实际只能使用16m内存,不过有一个办法可以解决,编辑AndroidManifest.xml中的Application节点,增加属性largeheap="true"参数.

 // 应用程序最大可用内存  dalvik.vm.heapsize的值我的手机是256M
        long maxMemory = ((int) Runtime.getRuntime().maxMemory())/1024/1024;  
        //应用程序已获得内存  dalvik.vm.heapgrowthlimit的值我的手机是25M
        long totalMemory = ((int) Runtime.getRuntime().totalMemory())/1024/1024;  
        //应用程序已获得内存中未使用内存  
        long freeMemory = ((int) Runtime.getRuntime().freeMemory())/1024/1024; 
        
	        System.out.println("---> maxMemory="+maxMemory+"M,totalMemory="+totalMemory+"M,freeMemory="+freeMemory+"M");
	    Toast.makeText(MainActivity.this, "---> maxMemory="+maxMemory+"M,totalMemory="+totalMemory+"M,freeMemory="+freeMemory+"M", Toast.LENGTH_SHORT).show();

JAVA的内存管理
大家都知道,android应用层是由java开发的,android的davlik虚拟机与jvm也类似,只不过它是基于寄存器的。因此要了解android的内存管理就必须得了解java的内存分配和垃圾回收机制。
在java中,是通过new关键字来为对象分配内存的,而内存的释放是由垃圾收集器(GC)来回收的,工程师在开发的过程中,不需要显式的去管理内存。但是这样有可能在不知不觉中就会浪费了很多内存,最终导致java虚拟机花费很多时间去进行垃圾回收,更严重的是造成JVM的OOM。因此,java工程师还是有必要了解JAVA的内存分配和垃圾回收机制。

1,内存结构

                                           

上面这张图是JVM的结构图,它主要四个部分组成:Class Loader子系统和执行引擎,运行时方法区和本地方法区,我们主要来看下RUNTIME DATA AREA区,也就是我们常说的JVM内存。从图中可以看出,RUNTIMEDATA AREA区主要由5个部分组成:
      Method Area:被装载的class的元信息存储在Method Area中,它是线程共享的
     Heap(堆):一个java虚拟机实例中只存在一个堆空间,存放一些对象信息,它是线程共享的
    Java栈: java虚拟机直接对java栈进行两种操作,以帧为单位的压栈和出栈(非线程共享)
     程序计数器(非线程共享)
    本地方法栈(非线程共享)

2,JVM的垃圾回收(GC)

       


JVM的垃圾原理是这样的,它把对象分为年轻代(Young)、年老代(Tenured)、持久代(Perm),对不同生命周期的对象使用不同的垃圾回收算法
年轻代(Young)
年轻代分为三个区,一个eden区,两个Survivor区。程序中生成的大部分新的对象都在Eden区中,当Eden区满时,还存活的对象将被复制到其中一个Survivor区,当此Survivor区的对象占用空间满了时,此区存活的对象又被复制到另外一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到年老代。
年老代(Tenured)
年老代存放的是上面年轻代复制过来的对象,也就是在年轻代中还存活的对象,并且区满了复制过来的。一般来说,年老代中的对象生命周期都比较长。
持久代(Perm)
用于存放静态的类和方法,持久代对垃圾回收没有显著的影响。

Android中内存泄露监测

在了解了JVM的内存管理后,我们再回过头来看看,在android中应该怎样来监测内存,从而看在应用中是否存在内存分配和垃圾回收问题而造成内存泄露情况。
在android中,有一个相对来说还不错的工具,可以用来监测内存是否存在泄露情况:DDMS—Heap


选择DDMS视图,并打开Devices视图和Heap视图
点击选择要监控的进程,比如:上图中我选择的是system_process
选中Devices视图界面上的"update heap" 图标
点击Heap视图中的"Cause GC" 按钮(相当于向虚拟机发送了一次GC请求的操作)
在Heap视图中选择想要监控的Type,一般我们会观察dataobject的 total size的变化,正常情况下total size的值会稳定在一个有限的范围内,也就说程序中的代码良好,没有造成程序中的对象不被回收的情况。如果代码中存在没有释放对象引用的情况,那么data object的total size在每次GC之后都不会有明显的回落,随着操作次数的增加而total size也在不断的增加。(说明:选择好data object后,不断的操作应用,这样才可以看出total size的变化)。如果totalsize确实是在不断增加而没有回落,说明程序中有没有被释放的资源引用。那么我们应该怎么来定位呢?

Android中内存泄露定位

Mat(memory analyzer tools)是我们常用的用来定位内存泄露的工具,如果你使用ADT,并且安装了MAT的eclipse插件,你需要做的是进入DDMS视图的Devices视图


点击"dump HPROF file"按钮,然后使用MAT分析下载下来的文件


关于MAT的使用可以参考:http://www.blogjava.net/rosen/archive/2010/06/13/323522.html
这位兄弟写的比较详细。

转自:http://blog.csdn.net/xieqibao/article/details/6707519

android的内存优化看另一篇文章:

Android内存优化的几种方案


3、为什么会内存泄露(Memory Leak)?
android通过android虚拟机来管理内存,程序员只管申请内存创建对象,创建完不再需要关心怎么释放对象内存,一切由虚拟机帮你搞定,然而虚拟机回收对象是有条件的。这里简单叙述下java内存管理机制,java虚拟机维护着一张当前对象关系的object tree,当GC发生时,虚拟机会从GC Roots 开始去扫描当前的对象树,发现通过任何reference chain(引用链)无法访问某个对象的时候,该对象即被回收。名词GC Roots正是分析这一过程的起点,例如JVM自己确保了对象的可到达性(那么JVM就是GC Roots),所以GC Roots就是这样在内存中保持对象可到达性的,一旦不可到达,即被回收。通常GC Roots是一个在current thread(当前线程)的call stack(调用栈)上的对象(例如方法参数和局部变量),或者是线程自身或者是system class loader(系统类加载器)加载的类以及native code(本地代码)保留的活动对象。所以GC Roots是分析对象为何还存活于内存中的利器。知道了什么样的对象GC才会回收后,再来学习下对象引用都包含哪些吧。
Java中包含4种对象引用:
   强引用: 通常我们编写的代码都是Strong Ref,eg :Person person = new Person("sunny");不管系统资源有多紧张,强引用的对象都绝对不会被回收,即使他以后不再用到。
   软引用只要有足够的内存,就一直保持对象。一般可用来实现缓存,通过java.lang.r.efSoftReference类实现。内存非常紧张的时候会被回收,其他时候不会被回收,所以在使用之前需要判空,从而判断当前时候已经被回收了。
   弱引用:通过WeakReference类实现,eg : WeakReference p = new WeakReference(new Person("Rain"));不管内存是否足够,系统垃圾回收时必定会回收。
   虚引用:不能单独使用,主要是用于追踪对象被垃圾回收的状态。通过PhantomReference类和引用队列ReferenceQueue类联合使用实现。

4、为什么会发生OOM(Out Of Memory)?
  OOM:即OutOfMemoery,顾名思义就是指内存溢出了。之前我们知道Android的应用程序所能申请的最大内存都是有限的,OOM是指APP向系统申请内存的请求超过了应用所能有的最大阀值的内存,系统无法再分配多余的空间,就会造成OOM error。在Android平台下,除了之前所说的持续发生了内存泄漏(Memory Leak),累积到一定程度导致OOM的情况以外,也有一次性申请很多内存,比如说一次创建大的数组或者是载入大的文件如图片的时候。实际中很多情况就是出现在图片不当处理加载的时候。

5、常见的MemoryLeak分析
后来看到了更多的MemoryLeak相关的知识,有了更多的实践经验,
就此小小总结了一下,见 Android 内存优化 (防Memory Leak)Android内存优化

作者:tuke_tuke 发表于2016/8/25 20:12:51 原文链接
阅读:65 评论:1 查看评论

CollapsingToolbarLayout使用

$
0
0

我们来看一下CollapsingToolbarLayout的使用场景。


CollapsingToolbarLayout

可以看到,Toolbar的标题放大并在下方显示,当我们向上滑动列表时,顶部Header部分的图片向上折叠隐藏,标题向上移动并缩小,同时以渐显式的方式显示蓝色主题,直至高度缩为Toolbar的高度并成为Toolbar的背景色;向下滑动列表时,Header部分逐渐显示。这个效果就是利用了CollapsingToolbarLayout控件,在讲解案例代码前,先来介绍一下CollapsingToolbarLayout。

CollapsingToolbarLayout在 CollapsingToolbarLayout 的 Children 布局上,可以按照 FrameLayout 的属性来拍版,因为它本身继承于 FrameLayout :CollapsingToolbarLayout is a wrapper for Toolbar which implements a collapsing app bar. It is designed to be used as a direct child of a AppBarLayout.从官方对CollapsingToolbarLayout的介绍上可以看出,CollapsingToolbarLayout 是对 Toolbar 的一个包装,以达到折叠 AppBar 的交互视觉效果。所以,CollapsingToolbarLayout 的使用一定离不开 AppBarLayout 和 Toolbar,并且作为 AppBarLayout 的直接子视图使用。关于CollapsingToolbarLayout的属性在官网上可以查到,这里我只介绍案例中我们常用的几个属性:title标题,布局展开时放大显示在图片底部,布局折叠时缩小显示在Toolbar左侧。注意,没有设置这个属性时,默认使用Toolbar的标题;statusBarScrim顶部视图折叠状态下,状态栏的遮罩色。通常这样设置:app:statusBarScrim="?attr/colorPrimaryDark",即style样式中定义的沉浸式状态栏颜色。这个属性要和getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);(支持API19及以上版本,位于setContentView语句前面)一起使用,使顶部视图展开时图片能够延伸到状态栏位置显示,如效果图中所示;contentScrim内容遮罩,上下滚动时图片上面显示和隐藏的遮罩色,Toolbar位置的的背景色;通常这样设置:app:contentScrim="?attr/colorPrimary",即显示为Toolbar颜色,应用的主题色;layout_collapseMode折叠模式,设置其他控件滚动时自身的交互行为,有两种取值:parallax,折叠视差效果,比如上述效果图中的图片;pin,固定别针效果,比如上图中的Toolbar;layout_collapseParallaxMultiplier不折叠视差系数,配合parallax模式使用,取值有点类似alpha(不透明度),在0.0 ~ 1.0之间,默认值为0.5。当设置为1.0,滚动列表时图片不会折叠移动;

代码实现:



关于CoordinatorLayout作为根布局容器如何协调子控件之间的交互行为,可以参考上一篇文章,这里我介绍一下本例中几个新的注意点。作为AppBarLayout的直接子控件,CollapsingToolbarLayout包裹Header部分的ImageView和Toolbar,并分别设置二者的折叠模式。这个例子,我们给CollapsingToolbarLayout的layout_scrollFlags属性值设为:scroll|exitUntilCollapsed,其中exitUntilCollapsed表示控件向上折叠退出并以最小高度停留在顶部;前面介绍CollapsingToolbarLayout属性时介绍到了statusBarScrim的使用,其实也可以通过android:fitsSystemWindows和values-v21中style样式的statusBarColor和windowDrawsSystemBarBackgrounds属性来完成状态栏的背景色变化,详情参考源码即可;通过layout_anchor和layout_anchorGravity可以控制FloatingActionButton的behavior和位置,如上图所示,当滚动列表是,FAB按钮会随着AppBarLayout而显示和隐藏,并自带缩放动画。

示例源码我在GitHub上建立了一个Repository,用来存放整个Android Material Design系列控件的学习案例,会伴随着文章逐渐更新完善,欢迎大家补充交流,

代码地址:github.com/xiangzhihong/MDStudySamples

作者:xiangzhihong8 发表于2016/8/25 20:16:59 原文链接
阅读:66 评论:0 查看评论

再谈STM32的CAN过滤器-bxCAN的过滤器的4种工作模式以及使用方法总结

$
0
0

1. 前言

bxCAN是STM32系列最稳定的IP核之一,无论有哪个新型号出来,这个IP核基本未变,可见这个IP核的设计是相当成熟的。本文所讲述的内容属于这个IP核的一部分,掌握了本文所讲内容,就可以很方便地适用于所有STM32系列中包含bxCAN外设的型号。有关bxCAN的过滤器部分的内容在参考手册中往往看得“不甚明白“,本文就过滤器的4种工作模式进行详细讲解并使用具体的代码进行演示,这些代码都进行过实测验证通过的,希望能给读者对于bxCAN过滤器有一个清晰的理解。

 

2. 准备工作

2.1.   为什么要过滤器?

在这里,我们可以将CAN总线看成一个广播消息通道,上面传输着各种类型的消息,好比报纸,有体育新闻,财经新闻,政治新闻,还有军事新闻,每个人都有自己的喜好,不一定对所有新闻都感兴趣,因此,在看报纸的时候,一般人都是只看自己感兴趣的那类新闻,而过滤掉其他不感兴趣的内容。那么我们一般是怎么过滤掉那些不感兴趣的内容的呢?下面有两种方法来实现这个目的:

第一种方法

         每次看报纸时,你都看下每篇文章的标题,如果感兴趣则继续看下去,如果不感兴趣,则忽略掉。

第二种方法

         你告诉邮递员,你只对财经新闻感兴趣,请只将财经类报纸送过来,其他的就不要送过来了,就这样,你看到的内容必定是你感兴趣的财经类新闻。

上面那种方法好呢?很明显,第二种方法是最好的,因为你不用自己每次判断哪些新闻内容是你感兴趣的,可以免受“垃圾”新闻干扰,从而可以节省时间忙其他事。bxCAN的过滤器就是采用上述第二种方法,你只需要设置好你感兴趣的那些CAN报文ID,那么MCU就只能收到这些CAN报文,是从硬件上过滤掉,完全不需要软件参与进来,从而节省了大大节省了MCU的时间,可以更加专注于其他事务,这个就是bxCAN过滤器的意义所在。

2.2.   两种过滤模式(列表模式与掩码模式)

假设我们是bxCAN这个IP的设计者,现在由我们来设计过滤器,那么我们该如何设计呢?

首先我们是不是很快就会想到只要准备好一张表,把我们需要关注的所有CAN报文ID写上去,开始过滤的时候只要对比这张表,如果接收到的报文ID与表上的相符,则通过,如果表上没有,则不通过,这个就是简单的过滤方案。恭喜你!bxCAN过滤器的列表模式采用的就是这种方案。

 

但是,这种列表方案有点缺陷,即如果我们只关注一个报文ID,则需要往列表中写入这个ID,如果需要关注两个,则需要写入两个报文ID,如果需要关注100个,则需要写入100个,如果需要1万个,那么需要写入1万个,可问题是,有这个大的列表供我们使用吗?大家都知道,MCU上的资源是有限的,不可能提供1万个或更多,甚至100个都嫌多。非常明显,这种列表的方式受到列表容量大小的限制,实际上,bxCAN的一个过滤器若工作在列表模式下,scale为32时,每个过滤器的列表只能写入两个报文ID,若scale为16时,每个过滤器的列表最多可写入4个CAN ID,由此可见,MCU的资源是非常非常有限的,并不能任我们随心所欲。因此,我们需要考虑另外一种替代方案,这种方案应该不受到数量限制。

 

下面假设我们是古时候一座城镇的守卫,城主要求只有1156年出生的人才可以进城,我们又该如何执行呢?假设古时候的人也有类似今天的身份证(...->_<-…),大家都知道,身份份证号码中有4位是表示出生年月,如下图:


图 1 18位身份证号码的各位定义

如上图,身份证中第7~10这4位数表示的是出生年份,那么,我们可以这么执行:

检查想要进城的所有人的身份证号码的第7~10位数字,如果这个数字依次为1156则可以进入,否则则不可以,至于身份证号码的其他位则完全不关心。假如过几天城主放宽进城条件为只要是1150年~1160前的人都可以进城,那么,我们就可以只关注身份证号码的第7~9这3位数是否为115就可以了,对不对?这样一来,我们就可以非常完美地执行城主的要求了。

 

再变下,假设现在使用机器来当守卫,不再是人来执行这个“筛选”工作。机器是死的,没有人那么灵活,那么机器又该如何执行呢?

对于机器来说,每一步都得细化到机器可以理解的程度,于是我们可以作如下细化:

第一步:获取想进城的人的身份证号码

第二步:只看获取到身份证的第7~9位,其他位忽略

第三步:将忽略后的结果与1156进行比较

第四步:比较结果相同则通过,不同则不能通过

这种方式,我们称之为掩码模式

2.3.   验证码与屏蔽码

仔细查看上面4个步骤,这不就是C代码中的if语句吗?如下:

if( x & y ==z) //x表示待检查身份证号码,y表示只关注第7~9位的屏蔽码,Z则为1156,这里叫做验证码
{
	//可以通过
}
else
{
	//不可以通过
}

对于机器来说,我们要为它准备好两张纸片,一片写上屏蔽码,另一片纸片写上验证码,屏蔽码上相应位为1时,表示此位需要与验证码对应位进行比较,反之,则表示不需要。机器在执行任务的时候先将获取的身份证号码与屏蔽码进行“与”操作,再将结果与验证码的进行比较,根据判断是否相同来决定是否通过。整个判别流程如下所示:

图 2 掩码模式的计算过程

从上图可以很容易地理解屏蔽码与验证码的含义,这样一来,能通过的结果数量就完全取决于屏蔽码,设得宽,则可以通过的多(所有位为0,则不过任何过滤操作,则谁都可以通过),设得窄,则通过的少(所有位设为1,则只有一个能通过)。那么知道这个有什么用呢?因为bxCAN的过滤器的掩码模式就是采用这种方式,在bxCAN中,分别采用了两个寄存器(CAN_FiR1,CAN_FiR2)来存储屏蔽码与验证码,从而实现掩码模式的工作流程的。这样,我们就知道了bxCAN过滤器的掩码模式的大概工作原理。

但是,我们得注意到,采用掩码模式的方式并不能精确的对每一个ID进行过滤,打个比方,还是采用之前的守卫的例子,假如城主要求只有1150~1158年出生的人能通过,那么,若我们还是才用掩码模式,那么掩码就设为第7~9位为”1”,对应的,验证码的7~9位分别为”115”,这样就可以了。但是,仔细一想,出生于1159的人还是可以通过,是不是?但总体来说,虽然没有做到精确过滤,但我们还是能做到大体过滤的,而这个就是掩码模式的缺点了。在实际应用时,取决于需求,有时我们会同时使用到列表模式和掩码模式,这都是可能的。


2.4.   列表模式与掩码模式的对比

综合之前所述,下面我们来对比一下列表模式与掩码模式这两种模式的优缺点。

模式优点缺点
列表模式能精确地过滤每个指定的CAN ID有数量限制
掩码模式取决于屏蔽码,有时无法完全精确到每一个CAN ID,部分不期望的CAN ID有时也会收到数量取决于屏蔽码,最多无上限

2.5.   标准CAN ID与扩展CAN ID

1986 年德国电气商BOSCH公司开发出面向汽车的CAN 通信协议,刚开始的时候,CAN ID定义为11位,我们称之为标准格式,ISO11898-1标准中CAN的基本格式如下图所示:

图 3 标准CAN报文格式定义


如上图所示,标准CAN ID存放在上图ID18~ID28中,共11位。随着工业发展,后来发现11位的CAN ID已经不够用,于是就增加了18位,扩展CAN ID到29位,如下图所示:

图 4 扩展CAN报文格式定义

从上图对比扩展CAN报文与标准CAN报文,发现在仲裁域部分,扩展CAN报文的CAN ID包含了base Identifier与extension Identifier,即基本ID与扩展ID,而标准CAN报文的CAN ID部分只包含基本ID,扩展ID(ID0~ID17)被放在基本ID的右方,也就是说,属于低位。知道这些有什么用呢?至少我们可以得到这两条信息:

  •  标准ID一般小于或等于<=0x7FF(11位),只包含基本ID。
  • 对于扩展CAN的低18位为扩展ID,高11位为基本ID。

例如标准CAN ID 0x7E1,二进制展开为0b 0[111 1110 0001] ,只有中括号内的11位才有效,其全部是基本ID。

再例如扩展CAN ID 0x1835f107,二进制展开为0b 000[1 1000 0011 10][01 11110001 0000 0111],只有红色中括号和绿色中括号内的位才有效,总共29位,左边红色中括号中的11位为基本ID,右边绿色中括号内的18位为扩展ID请记住这个信息!知道这个之后,我们可以很方便地将一个CANID拆分成基本ID和扩展ID,这个也将在后续的内容中多次用到,再次留意一下,扩展ID是位于基本ID的右方,在扩展CAN ID的构成中,扩展ID位于18位,而基本ID位于11位,于是要获取一个扩展CANID的基本ID,就只需要将这个CANID右移18(这种算法后续将多次用到,请务必记住!)

3. bxCAN的过滤器的解决方案

终于进入到正题了!前面已经介绍了过滤器的列表模式与掩码模式,以及掩码模式下的屏蔽码与验证码的含义,还介绍了标准CAN ID与扩展CAN ID的组成部分。现在我们终于要站在bxCAN的角度来分析其过滤方案。

首先过滤模式分列表模式和掩码模式,因此,对于没有过滤器,我们需要这么一个位来标记,用户可以通过设置这个位来标记他到底是想要这个过滤器工作在列表模式下还是掩码模式,于是,这个表示过滤模式的位就定义在CAN_FM1R寄存器中的FBMx位上,如下图:

图5 CAN过滤器模式寄存器CAN_FM1R定义


这里以STM32F407为例,bxCAN共有28个过滤器,于是上图的每一个位对应地表示这28个过滤器的工作模式,供用户设置。”0”表示掩码模式,”1”表示列表模式。

 

另外,我们知道了标准CAN ID位11位,而扩展CAN ID有29位,对于标准的CAN ID来说,我们有一个16位的寄存器来处理他足够了,相应地,扩展CAN ID,我们就必须使用32位的寄存器来处理它,而在实际应用中,根据需求,我们可能自始至终都只需要处理11位的CAN ID。对于资源严重紧张的MCU环境来说,本着不浪费的原则,这里最好能有另外一个标志用告诉过滤器是否需要处理32位的CAN ID。于是,bxCAN处于这种考虑,也设置了这么一个寄存器CAN_FS1R来表示CAN ID的位宽,如下图所示:

图6 CAN过滤器位宽寄存器CAN_FS1R定义


如上图,每一个位对应着bxCAN中28个过滤器的位宽,这个需要用户来设置。

于是根据模式与位宽的设置,我们共可以得出4中不同的组合:32位宽的列表模式,16位宽的列表模式,32位宽掩码模式,16位宽的掩码模式。如下图所示:

图 7 CAN过滤器的4中工作模式


在bxCAN中,每个过滤器都存在这么两个寄存器CAN_FxR1和CAN_FxR2,这两个寄存器都是32位的,他的定义并不是固定的,针对不同的工作模式组合他的定义是不一样的,如列表模式-32位宽模式下,这两个寄存器的各位定义都是一样的,都用来存储某个具体的期望通过的CAN ID,这样就可以存入2个期望通过的CAN ID(标准CAN ID和扩展CAN ID均可);若在掩码模式-32位宽模式下时,则CAN_FxR1用做32位宽的验证码,而CAN_FxR2则用作32位宽的屏蔽码。在16位宽时,CAN_FxR1和CAN_FxR2都要各自拆分成两个16位宽的寄存器来使用,在列表模式-16位宽模式下,CAN_FxR1和CAN_FxR2定义一样,且各自拆成两个,则总共可以写入4个标准CAN ID,若在16位宽的掩码模式下,则可以当做2对验证码+屏蔽码组合来用,但它只能对标准CAN ID进行过滤。这个就是bxCAN过滤器的解决方案,它采用了这4种工作模式。

 

本着从易到难得目的,下面我们将依次介绍如何使用bxCAN的这4种工作模式并给出对应的代码示例.

4. 应用实例

4.1.   工程建立及主体代码

本文硬件采用STM3240G-EVAL评估板和ZLG的USBCAN-2E-U及其配套的软件工具CANTest来实现对MCU进行CAN报文的发送。工程使用STM32CubeMx自动生成:

 

引脚如下:

PD0: CAN1_Rx

PD1: CAN1_Tx

PG6: LED1

PG8: LED2

PI9:  LED3

PC7: LED4

图 8 引脚定义


时钟树如下设置:



图 9时钟树设置

在配置中的NVIC中,打开CAN1 RX0接收中断,如下图所示:



图 10 打开CAN1的RX0接收中断


其他的没有什么特殊设置,生成工程后的main函数如下:

int main(void)
{

  /* USER CODE BEGIN 1 */
  static CanTxMsgTypeDef        TxMessage;
  static CanRxMsgTypeDef        RxMessage;
  /* USER CODE END 1 */

  /* MCU Configuration----------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* Configure the system clock */
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_CAN1_Init();

  /* USER CODE BEGIN 2 */
  hcan1.pTxMsg =&TxMessage;
  hcan1.pRxMsg =&RxMessage;
  CANFilterConfig_Scale32_IdList();			//列表模式-32位宽
//CANFilterConfig_Scale16_IdList();			//列表模式-16位宽
//CANFilterConfig_Scale32_IdMask_StandardIdOnly();	//掩码模式-32位宽(只有标准CAN ID)
//CANFilterConfig_Scale32_IdMask_ExtendIdOnly();	//掩码模式-32位宽(只用扩展CAN ID)
//CANFilterConfig_Scale32_IdMask_StandardId_ExtendId_Mix(); //掩码模式-32位宽(标准CANID与扩展CAN ID混合)
//CANFilterConfig_Scale16_IdMask();			//掩码模式-16位宽
  HAL_CAN_Receive_IT(&hcan1,CAN_FIFO0);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

}

如上代码所示,示例中将采用各种过滤器配置来演示,在测试时我们可以只保留一种配置,也可以全部打开,为了确保每种配置的准确性,这里建议只保留其中一种配置进行测试。

另外,接收中断回调函数如下所示:

void HAL_CAN_RxCpltCallback(CAN_HandleTypeDef* hcan)
{
  if(hcan->pRxMsg->StdId ==0x321)
  {
    //handle the CAN message
    HandleCANMessage(hcan->pRxMsg);		//处理接收到的CAN报文
  }
  if(hcan->pRxMsg->ExtId ==0x1800f001)
  {
     HandleCANMessage(hcan->pRxMsg);		//处理接收到的CAN报文
  }
  HAL_GPIO_WritePin(LED4_GPIO_Port,LED4_Pin,GPIO_PIN_SET);    //若收到消息则闪烁下LED4
  HAL_Delay(200);
  HAL_GPIO_WritePin(LED4_GPIO_Port,LED4_Pin,GPIO_PIN_RESET);  
  HAL_CAN_Receive_IT(&hcan1,CAN_FIFO0);
}

接下来将分别介绍过滤器的4中工作模式以及所对应的代码示例。

4.2.   32位宽的列表模式


11 32位宽下的CAN_FxR1与CAN_FxR2各位定义


如上图所示,在32位宽的列表模式下,CAN_FxR1与CAN_FxR2都用来存储希望通过的CAN ID,由于是32位宽的,因此既可以存储标准CAN ID,也可以存储扩展CAN ID。注意看上图最底下的各位定义,可以看出,从右到左,首先,最低位是没有用的,然后是RTR,表示是否为远程帧,接着IDE,扩展帧标志,然后才是EXID[0:17]这18位扩展ID,最后才是STID[0:10]这11位标准ID,也就是前面所说的基本ID。在进行配置的时候,即将希望通过的CAN ID写入的时候,要注意各个位对号入座,即基本ID放到对应的STD[0:10],扩展ID对应放到EXID[0:17],若是扩展帧,则需要将IDE设为“1”,标准帧则为“0”,数据帧设RTR为“0”,远程帧设RTR为“1”。示例代码如下:

static void CANFilterConfig_Scale32_IdList(void)
{
  CAN_FilterConfTypeDef  sFilterConfig;
  uint32_t StdId =0x321;				//这里写入两个CAN ID,一个位标准CAN ID
  uint32_t ExtId =0x1800f001;			//一个位扩展CAN ID
  
  sFilterConfig.FilterNumber = 0;				//使用过滤器0
  sFilterConfig.FilterMode = CAN_FILTERMODE_IDLIST;		//设为列表模式
  sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;	//配置为32位宽
  sFilterConfig.FilterIdHigh = StdId<<5;			//基本ID放入到STID中
  sFilterConfig.FilterIdLow = 0|CAN_ID_STD;			//设置IDE位为0
  sFilterConfig.FilterMaskIdHigh = ((ExtId<<3)>>16)&0xffff;
  sFilterConfig.FilterMaskIdLow = (ExtId<<3)&0xffff|CAN_ID_EXT;	//设置IDE位为1
  sFilterConfig.FilterFIFOAssignment = 0;			//接收到的报文放入到FIFO0中
  sFilterConfig.FilterActivation = ENABLE;
  sFilterConfig.BankNumber = 14;
  
  if(HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK)
  {
    Error_Handler();
  }
}

这里需要说明一下,由于我们使用的是cube库,在cube库中,CAN_FxR1与CAN_FxR2寄存器分别被拆成两段,CAN_FxR1寄存器的高16位对应着上面代码中的FilterIdHigh,低16位对应着FilterIdLow,而CAN_FxR2寄存器的高16位对应着FilterMaskIdHigh,低16位对应着FilterMaskIdLow,这个CAN_FilterConfTypeDef的的4个成员FilterIdHigh,FilterIdLow,FilterMaskIdHigh,FilterMaskIdLow,不应该单纯看其名字,被其名字误导,而应该就单纯地将这4个成员看成4个uint_16类型的变量x,y,m,n而已,后续其他示例也是同样理解,不再重复解释。这4个16位的变量其具体含义取决于当前过滤器工作与何种模式,比如当前32位宽的列表模式下,FilterIdHigh与FilterIdLow一起用来存放一个CAN ID,FilterMaskIdHigh与FilterMaskIdLow用来存放另一个CAN ID,不再表示其字面所示的mask含义,这点我们需要特别注意。

 

在上述代码示例中,我们分别将标准CAN ID和扩展CAN ID放入到CAN_FxR1与CAN_FxR2寄存器中。对于标准CAN ID,对比图11,由于标准CAN ID只拥有标准ID,所以,只需要将标准ID放入到高16位的STID[0:10]中,高16位最右边被EXID[13:17]占着,因此,需要将StdId左移5位才能刚好放入到CAN_FxR1的高16位中,于是有了:

sFilterConfig.FilterIdHigh = StdId<<5;

另一个扩展CAN ID ExtId类型,将其基本ID放入到STID中,扩展ID放入到EXID中,最后设置IDE位为1。就这样配置好了。

4.3.   16位宽的列表模式

图12 16位宽的列表模式


如上图所示,在16位宽的列表模式下,FilterIdHigh,FilterIdLow,FilterMaskIdHigh,FilterMaskIdLow这4个16位变量都是用来存储一个标准CAN ID,这样,就可以存放4个标准CAN ID了,需要注意地是,此种模式下,是不能处理扩展CANID,凡是需要过滤扩展CAN ID的,都是需要用到32位宽的模式。于是有以下代码示例:

static void CANFilterConfig_Scale16_IdList(void)
{
  CAN_FilterConfTypeDef  sFilterConfig;
  uint32_t StdId1 =0x123;						//这里采用4个标准CAN ID作为例子
  uint32_t StdId2 =0x124;
  uint32_t StdId3 =0x125;
  uint32_t StdId4 =0x126;
  
  sFilterConfig.FilterNumber = 1;				//使用过滤器1
  sFilterConfig.FilterMode = CAN_FILTERMODE_IDLIST;		//设为列表模式
  sFilterConfig.FilterScale = CAN_FILTERSCALE_16BIT;	//位宽设置为16位
  sFilterConfig.FilterIdHigh = StdId1<<5;	 //4个标准CAN ID分别放入到4个存储中
  sFilterConfig.FilterIdLow = StdId2<<5;
  sFilterConfig.FilterMaskIdHigh = StdId3<<5;
  sFilterConfig.FilterMaskIdLow = StdId4<<5;
  sFilterConfig.FilterFIFOAssignment = 0;			//接收到的报文放入到FIFO0中
  sFilterConfig.FilterActivation = ENABLE;
  sFilterConfig.BankNumber = 14;
  
  if(HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK)
  {
    Error_Handler();
  }
}

可见,列表模式还是非常好理解的。

4.4.   32位宽掩码模式


图13 32位宽掩码模式


如上图所示,32位宽模式下,FilterIdHigh与FilterIdLow合在一起表示CAN_FxR1寄存器,用来存放验证码,而FilterMaskIdHigh与FilterMaskIdLow合在一起表示CAN_FxR2寄存器,用来存放屏蔽码,关于验证码与屏蔽码的概念在之前的2.3节已经明确说明了,不清楚的可以回过去看看2.3节的内容。在32位宽的掩码模式下,既可以过滤标准CAN ID,也可以过滤扩展CAN ID,甚至两者混合这来也是可以的,下面我们就这3中情况分别给出示例。

4.4.1. 只针对标准CAN ID

如下代码示例:

static void CANFilterConfig_Scale32_IdMask_StandardIdOnly(void)
{
  CAN_FilterConfTypeDef  sFilterConfig;
  uint16_t StdIdArray[10] ={0x7e0,0x7e1,0x7e2,0x7e3,0x7e4,
                                0x7e5,0x7e6,0x7e7,0x7e8,0x7e9}; //定义一组标准CAN ID
  uint16_t      mask,num,tmp,i;
  
  sFilterConfig.FilterNumber = 2;				//使用过滤器2
  sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;		//配置为掩码模式
  sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;	//设置为32位宽
  sFilterConfig.FilterIdHigh =(StdIdArray[0]<<5);		//验证码可以设置为StdIdArray[]数组中任意一个,这里使用StdIdArray[0]作为验证码
  sFilterConfig.FilterIdLow =0;
  
  mask =0x7ff;						//下面开始计算屏蔽码
  num =sizeof(StdIdArray)/sizeof(StdIdArray[0]);
  for(i =0; i<num; i++)		//屏蔽码位StdIdArray[]数组中所有成员的同或结果
  {
    tmp =StdIdArray[i] ^ (~StdIdArray[0]);	//所有数组成员与第0个成员进行同或操作
    mask &=tmp;
  }
  sFilterConfig.FilterMaskIdHigh =(mask<<5);
  sFilterConfig.FilterMaskIdLow =0|0x02; 		//只接收数据帧
  
  sFilterConfig.FilterFIFOAssignment = 0;		//设置通过的数据帧进入到FIFO0中
  sFilterConfig.FilterActivation = ENABLE;
  sFilterConfig.BankNumber = 14;
  
  if(HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK)
  {
    Error_Handler();
  }
}

如上代码所示,对于验证码,任意一个期望通过的CAN ID都是可以设为验证码的,但屏蔽码,却是所有期望通过的CAN ID相互同或后的最终结果,这个即是屏蔽码。

4.4.2. 只针对扩展CAN ID

如下代码示例:

static void CANFilterConfig_Scale32_IdMask_ExtendIdOnly(void)
{
  CAN_FilterConfTypeDef  sFilterConfig;
  //定义一组扩展CAN ID用来测试
uint32_t ExtIdArray[10] ={0x1839f101,0x1835f102,0x1835f113,0x1835f124,0x1835f105,
                            0x1835f106,0x1835f107,0x1835f108,0x1835f109,0x1835f10A};
  uint32_t      mask,num,tmp,i;
  
  sFilterConfig.FilterNumber = 3;					//使用过滤器3
  sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;			//配置为掩码模式
  sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;		//设为32位宽
  sFilterConfig.FilterIdHigh =((ExtIdArray[0]<<3) >>16) &0xffff;//数组任意一个成员都可以作为验证码
  sFilterConfig.FilterIdLow =((ExtIdArray[0]<<3)&0xffff) | CAN_ID_EXT;
  
  mask =0x1fffffff;
  num =sizeof(ExtIdArray)/sizeof(ExtIdArray[0]);
  for(i =0; i<num; i++)				//屏蔽码位数组各成员相互同或的结果
  {
    tmp =ExtIdArray[i] ^ (~ExtIdArray[0]);	//都与第一个数据成员进行同或操作
    mask &=tmp;
  }
  mask <<=3;    								//对齐寄存器
  sFilterConfig.FilterMaskIdHigh = (mask>>16)&0xffff;
  sFilterConfig.FilterMaskIdLow = (mask&0xffff)|0x02; 		//只接收数据帧
  sFilterConfig.FilterFIFOAssignment = 0;
  sFilterConfig.FilterActivation = ENABLE;
  sFilterConfig.BankNumber = 14;
  
  if(HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK)
  {
    Error_Handler();
  }
}

如上代码所示,与之前的标准CAN ID相比,扩展CAN ID的验证码与屏蔽码放入到相对应的寄存器时所移动的位数与标准CAN ID时有所差别,其他的都一样。

接下来是标准CAN ID与扩展CAN ID混合着来。

4.4.3. 标准CAN ID与扩展CAN ID混合过滤

如下代码所示:

static void CANFilterConfig_Scale32_IdMask_StandardId_ExtendId_Mix(void)
{
  CAN_FilterConfTypeDef  sFilterConfig;
  //定义一组标准CAN ID
uint32_t StdIdArray[10] ={0x711,0x712,0x713,0x714,0x715,
                          0x716,0x717,0x718,0x719,0x71a};
  //定义另外一组扩展CAN ID
uint32_t ExtIdArray[10] ={0x1900fAB1,0x1900fAB2,0x1900fAB3,0x1900fAB4,0x1900fAB5,
                            0x1900fAB6,0x1900fAB7,0x1900fAB8,0x1900fAB9,0x1900fABA};
  uint32_t      mask,num,tmp,i,standard_mask,extend_mask,mix_mask;
  
  sFilterConfig.FilterNumber = 4;				//使用过滤器4
  sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;		//配置为掩码模式
  sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;	//设为32位宽
  sFilterConfig.FilterIdHigh =((ExtIdArray[0]<<3) >>16) &0xffff;	//使用第一个扩展CAN  ID作为验证码
  sFilterConfig.FilterIdLow =((ExtIdArray[0]<<3)&0xffff);
  
  standard_mask =0x7ff;		//下面是计算屏蔽码
  num =sizeof(StdIdArray)/sizeof(StdIdArray[0]);
  for(i =0; i<num; i++)			//首先计算出所有标准CAN ID的屏蔽码
  {
    tmp =StdIdArray[i] ^ (~StdIdArray[0]);
    standard_mask &=tmp;
  }
  
  extend_mask =0x1fffffff;
  num =sizeof(ExtIdArray)/sizeof(ExtIdArray[0]);
  for(i =0; i<num; i++)			//接着计算出所有扩展CAN ID的屏蔽码
  {
    tmp =ExtIdArray[i] ^ (~ExtIdArray[0]);
    extend_mask &=tmp;
  }
  mix_mask =(StdIdArray[0]<<18)^ (~ExtIdArray[0]);	//再计算标准CAN ID与扩展CAN ID混合的屏蔽码
  mask =(standard_mask<<18)& extend_mask &mix_mask;	//最后计算最终的屏蔽码
  mask <<=3;    						//对齐寄存器

  sFilterConfig.FilterMaskIdHigh = (mask>>16)&0xffff;
  sFilterConfig.FilterMaskIdLow = (mask&0xffff);
  sFilterConfig.FilterFIFOAssignment = 0;
  sFilterConfig.FilterActivation = ENABLE;
  sFilterConfig.BankNumber = 14;
  
  if(HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK)
  {
    Error_Handler();
  }
}

如上代码所示,在混合的情况下,只需稍微修改下屏蔽码的计算方式就可以了,其他的基本没有什么变化。

4.5.   16位宽掩码模式

如下图所示:

图14 16位宽的掩码模式


如上图所示,此图是从STM32F405的参考文档RM0090截取过来,版本为DocID018909 Rev12,但是经过我的严格测试,发现此图是有问题的,在16位宽的掩码模式下,CAN_FxR1的低16位是作为验证码,对应的16位屏蔽码为CAN_FxR2的低16位,而不是CAN_FxR1的高16位,同样的,CAN_FxR1的高16位是作为验证码,对应与CAN_FxR2的高16位为屏蔽码。从测试结果来看,上图的CAN_FxR1[16:31]需要与CAN_FxR2[0:15]对换位置即可,即CAN_FxR1的低16位的CAN_FxR2的低16位是一对组合,即CAN_FxR1的高16位的CAN_FxR2的高16位是另外一对这,即“低对低,高对高”才是正确的。在这种模式下,有两对验证码与屏蔽码组合,都只能对标准CAN ID进行过滤,于是,其示例代码如下:

static void CANFilterConfig_Scale16_IdMask(void)
{
  CAN_FilterConfTypeDef  sFilterConfig;
  uint16_t StdIdArray1[10] ={0x7D1,0x7D2,0x7D3,0x7D4,0x7D5,	//定义第一组标准CAN ID
                          0x7D6,0x7D7,0x7D8,0x7D9,0x7DA};
  uint16_t StdIdArray2[10] ={0x751,0x752,0x753,0x754,0x755,	//定义第二组标准CAN ID
                          0x756,0x757,0x758,0x759,0x75A};
  uint16_t      mask,tmp,i,num;
  
  sFilterConfig.FilterNumber = 5;					//使用过滤器5
  sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;			//配置为掩码模式
  sFilterConfig.FilterScale = CAN_FILTERSCALE_16BIT;		//设为16位宽
  
  //配置第一个过滤对
  sFilterConfig.FilterIdLow =StdIdArray1[0]<<5;			//设置第一个验证码
  mask =0x7ff;
  num =sizeof(StdIdArray1)/sizeof(StdIdArray1[0]);
  for(i =0; i<num; i++)							//计算第一个屏蔽码
  {
    tmp =StdIdArray1[i] ^ (~StdIdArray1[0]);
    mask &=tmp;
  }
  sFilterConfig.FilterMaskIdLow =(mask<<5)|0x10;    //只接收数据帧
  
  //配置第二个过滤对
  sFilterConfig.FilterIdHigh = StdIdArray2[0]<<5;	//设置第二个验证码
  mask =0x7ff;
  num =sizeof(StdIdArray2)/sizeof(StdIdArray2[0]);
  for(i =0; i<num; i++)					//计算第二个屏蔽码
  {
    tmp =StdIdArray2[i] ^ (~StdIdArray2[0]);
    mask &=tmp;
  }
  sFilterConfig.FilterMaskIdHigh = (mask<<5)|0x10;  //只接收数据帧
  
 
  sFilterConfig.FilterFIFOAssignment = 0;		//通过的CAN 消息放入到FIFO0中
  sFilterConfig.FilterActivation = ENABLE;
  sFilterConfig.BankNumber = 14;
  
if(HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK)
  {
    Error_Handler();
  }
}

如上代码所示,在这种模式下,其特殊之处就是可以配置两套验证码,屏蔽码组合,可以分别相对独立地对标准CAN ID进行过滤。

4.6.   测试验证

上述代码运行的STM3240G-EVAL评估板上,使用ZLG的USBCAN-2E-U盒子配合PC上的软件CANTest进行验证,整个系统连接后的效果如下图所示:

图 15 测试环境


测试时,逐个测试各个配置,并使用PC端软件CANTest发送各个测试的CAN ID均能通过,而使用其他的CAN ID则不能通过,测试结果正常.

5. 总结

在实际的应用中,我们需要根据需求的实际情况来决定使用何种过滤配置,STM32F4的bxCAN提供了28个过滤器,在配置之前,我们需要先将那些需要通过的CANID进行整理,若数量少,则使用列表模式,精准,若只有标准CAN ID,则可以考虑使用16位宽模式,若需求中的CAN ID过多,则可以考虑使用多个过滤器,部分使用列表模式,部分使用掩码模式,CAN ID值相近的可以归纳成一组,使用掩码模式进行过滤。但使用掩码模式的同时,我们也需要意识到,也有可能部分不期望的CAN ID也会通过过滤器,掩码放得越宽,带进其他CAN ID的几率就越大,这点我们需要格外注意,视情况进行应用判断和处理。另外,对于相近的CAN ID,我们可以提前计算好屏蔽码,直接在代码中填入,而不是在代码中临时计算,这样可以提高软件效率,大家视情况而定。


示例代码下载地址:http://download.csdn.net/detail/flydream0/9613116


作者:flydream0 发表于2016/8/25 21:02:36 原文链接
阅读:74 评论:0 查看评论

深入浅出再谈Unity内存泄漏

$
0
0

作者:Arthuryu,腾讯高级开发工程师
著作权归作者所有。商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。

WeTest导读

本文通过对内存泄漏(what)及其危害性(why)的介绍,引出在Unity环境下定位和修复内存泄漏的方法和工具(how)。最后提出了一些避免泄漏的方法与建议。

在之前推送的文章《内存是手游的硬伤——腾讯游戏谈Unity游戏Mono内存管理及泄漏问题》中,已经对腾讯游戏在Unity游戏开发过程中常见的Mono内存管理问题进行了介绍,收到了很多用户的反馈,希望能够更全面的介绍关于unity内存管理的问题。本期微信推送腾讯WeTest团队邀请到了公司中资深的测试专家Arthuryu,对Unity内存泄漏进行一个更加系统的介绍。

内存泄漏及其危害

相信各位程序猿们或多或少都会听到过内存泄漏这个名词,但是对于一些新手猿来说,或许不是很了解。内存泄漏?是内存漏出来了么?和霸气侧漏一样么?让我们先来看一下wikipedia的定义:

这里写图片描述

看了一遍冗长的定义,或许各位猿们心中就是一个大写的“晕”字。让我们打一个通俗的比方来解释下这个定义。

内存泄漏,可以通俗解释为“借银行钱不还”。在计算机的二进制世界里,操作系统就是银行;每一笔贷款,都是一次内存的申请;而你,就是一个应用程序。即银行贷款 = 应用程序操作系统申请内存。当然,在计算机世界中,我们需要感谢操作系统,因为他是一个不收利息的银行,你借了多少内存,你就只需要还回多少内存。那么我们可以总结一下,内存泄漏的简单定义,就是申请了内存,却没有在该释放的时候释放

如果你总是贷款而不还钱,那么银行里的钱就越来越少,最终导致其他人要借钱时,就无钱可借了。现实生活中,银行为了避免无钱可接,就会把总是借钱不还的人拉入黑名单,不再借他钱;而操作系统则更加凶残,他会直接“做了你”,操作系统将会直接kill掉应用程序。由此可以看出,内存泄漏的危害性与严重性,如果持续泄漏,将因内存占用过大而导致应用崩溃。当然泄漏还有其他的危害,例如内存被无用对象占用,导致接下来的内存分配需要更高的时间成本,从而造成游戏的卡顿等等。

这里写图片描述

Unity中的内存泄漏

在对内存泄漏有一个基本印象之后,我们再来看一下在特定环境——Unity下的内存泄漏。大家都知道,游戏程序由代码和资源两部分组成,Unity下的内存泄漏也主要分为代码侧的泄漏和资源侧的泄漏,当然,资源侧的泄漏也是因为在代码中对资源的不合理引用引起的。

代码中的泄漏 – Mono内存泄漏

熟悉Unity的猿类们应该都知道,Unity是使用基于Mono的C#(当然还有其他脚本语言,不过使用的人似乎很少,在此不做讨论)作为脚本语言,它是基于Garbage Collection(以下简称GC)机制的内存托管语言。那么既然是内存托管了,为什么还会存在内存泄漏呢?因为GC本身并不是万能的,GC能做的是通过一定的算法找到“垃圾”,并且自动将“垃圾”占用的内存回收。那么什么是垃圾呢?
我们先来看一下wikipedia上对于GC实现的简介:
这里写图片描述

定义还是过于冗长,我们来联想一下生活中,我们一般把没有利用价值的东西,称为垃圾,也就是没有用的东西,就是垃圾。在GC的世界中,也是一样的,没有引用的东西,就是“垃圾”。因为没有引用了,就意味着对于其他任何对象而言,都认为目标对象对我已经没有利用价值了,那它就是“垃圾”了。根据GC的机制,其占用的内存就会被回收。
基于以上的知识,我们很容易就可以想到为什么在托管内存的环境下,还是会出现内存泄漏了。这就像现实生活中的宅男宅女,吃了泡面总是忘记把盒子扔到门外的垃圾箱里;从计算机的角度来说,则是,在某对象超出其作用域时,我们 “忘记”清除对该无用对象的引用了。
说到这,有的同学可能会有疑问:我每次在代码中申请的内存都非常小,少则几B,多则几十K,现在设备的内存都比较大(几百M还是有的吧),即使泄漏会产生什么大影响么?
首先,水滴石穿的典故相信大家都知道,实际代码中,并非只有显示调用new才会分配内存,很多隐式的分配是不容易被发现的,例如产生一个List来存储数据,缓存了服务器下发的一份配置,产生一个字符串等等,这些操作都会产生内存的分配。你分配几十K,他分配几十K,一会儿内存就没了。
其次,有一点需要说明的是,在Unity环境下,Mono堆内存的占用,是只会增加不会减少的。具体来说,可以将Mono堆,理解为一个内存池,每次Mono内存的申请,都会在池内进行分配;释放的时候,也是归还给池,而不会归还给操作系统。如果某次分配,发现池内内存不够了,则会对池进行扩建——向操作系统申请更多的内存扩大池以满足该次的内存分配。需要注意的是,每次对池的扩建,都是一次较大的内存分配,每次扩建,都会将池扩大6-10M左右(此处无官方数据,是观察所得)。

这里写图片描述

上图是某游戏经过Cube测试的结果,可以看到Mono堆内存为39M左右,而建议值一般为 50M。
我们必须知道,Mono内存泄漏是Unity游戏开发中需要特别重视的部分。

资源中的泄漏 – Native内存泄漏

资源泄漏,顾名思义,是指将资源加载之后占有了内存,但是在资源不用之后,没有将资源卸载导致内存的无谓占用。
同样的,在讨论资源内存泄漏的原因之前,我们先来看一下Unity的资源管理与回收方式。为什么要将资源内存和代码内存分开讨论,也是因为其内存管理方式存在不同的原因。

上文中说的代码分配的内存,是通过Mono虚拟机,分配在Mono堆内存上的,其内存占用量一般较小,主要目的是程序猿在处理程序逻辑时使用;而Unity的资源,是通过Unity的C++层,分配在Native堆内存上的那部分内存。举个简单的例子,通过UnityEngine命名空间中的接口分配的内存,将会通过Unity分配在Native堆;通过System命名空间中的接口分配的内存,将会通过Mono Runtime分配在Mono堆。
这里写图片描述

了解了分配与管理方式的区别,我们再来看看回收的方式。如上文所说,Mono内存是通过GC来回收的,而Unity也提供了一种类似的方式来回收内存。不同的是,Unity的内存回收是需要主动触发的。就好比说,我们把垃圾扔在门口的垃圾桶里,GC是每天来看一次,有垃圾就收走;而Unity则需要你打个电话给它,通知它有垃圾要回收,它才会来。主动调用的接口是Resources.UnloadUnusedAssets()。其实GC也提供了同样的接口GC.Collect()
用来主动触发垃圾回收,这两个接口都需要很大的计算量,我们不建议在游戏运行时时不时主动调用一番,一般来说,为了避免游戏卡顿,建议在加载环节来处理垃圾回收的操作。有一点需要说明的是,Resources.UnloadUnusedAssets()内部本身就会调用GC.Collect()。Unity还提供了另外一个更加暴力的方式——Resources.UnloadAsset()来卸载资源,但是这个接口无论资源是不是“垃圾”,都会直接删除,是一个很危险的接口,建议确定资源不使用的情况下,再调用该接口。

基于上述基础知识,我们再来看一下为什么会有资源的泄漏。首先和代码侧的泄漏一样,由于“存在该释放却没有释放的错误引用”,导致回收机制认为目标对象不是“垃圾”,以至于不能被回收,这也是最常见的一种情况。

针对资源,还有一种典型的泄漏情况。由于资源卸载是主动触发的,那么清除对资源引用的时机就显得尤为重要。现在游戏的逻辑趋于复杂化,同时如果有新成员加入项目组,也未必能够清楚地了解所有资源管理的细节,如果“在触发了资源卸载之后,才清除对资源引用”,同样也会出现内存泄漏了。
赶上了资源回收
赶上了资源回收
错过了资源回收
错过了资源回收

还有一种资源上的泄漏,是因为Unity的一些接口在调用时会产生一份拷贝(例如Renderer.Material参考https://docs.unity3d.com/ScriptReference/Renderer-material.html),如果在使用上不注意的话,运行时会产生较多的资源拷贝,造成内存的无端浪费。但是此类内存拷贝一般量较少,修复起来也比较简单,这里不做大篇幅的介绍。

修复内存泄漏

根据上文描述,我们知道只要在回收到来之前,将引用解开就可以避免内存泄漏了,似乎是个很简单的问题。但是由于实际项目的逻辑复杂度往往超出想象,引用关系也不是简单的一层两层(有时候往往会多达十几层,甚至数十层才连接到最终的引用对象),并且可能存在交叉引用、环状引用等复杂情况,单纯从代码review的角度,是很难正确地解开引用的。如何查找导致泄漏的引用,是修复泄漏的难点和重点,也是本文主要想介绍的部分,下面就针对如何查找引用介绍一些思路和方法。至于时序问题,比较简单,在此不做赘述。

New Memory Profiler For Unity5

Unity的Memory Profiler一直就是一个被用户诟病的地方,对于内存的使用量,被谁使用等信息,没有很好的反映。Unity5作为最新一代的Unity产品,对于这个弱点进行了一些补强,推出了新一代的内存分析工具,较好地解决了上述问题。但是没有提供两次(或多次)内存快照的比较功能,这点比较遗憾。
注:内存快照比较是寻找内存泄漏的常用手段,将两次内存的状态截取出来,进行比较,可以清楚地发现内存的变化,寻找内存的增量与泄漏点。一般会在游戏进关前以及出关后做两次dump,其中新增的内存分配,可以视为泄漏。
这里写图片描述
这里写图片描述

由于是Unity官方的工具,网上有比较详细的使用教程,在此不加赘述,可以参考下列链接或Google:
Unity-Technologies MemoryProfiler
memoryprofiler intro
由于Unity5普及度及稳定性还有待提升,公司内普遍还是4.x的环境,那么上述的新工具就不适用了。有的同学说,升级一个5的工程来做Memory Profile嘛,这个当然也可以,不过Unity5对于4的兼容性不太好,升级过程中需要修改不少东西,维护两个工程也是比较麻烦的事。

那么,下面就给出两个在Unity4环境下也可以使用的泄漏追踪工具。

Mono内存的放大镜——Cube

Cube是 腾讯游戏下的腾讯WeTest平台上针对Unity项目的性能指标收集工具,通过Cube可以较方便地获取到游戏的各项性能指标,为性能优化提供了方向。同时Cube也是游戏性能一个很好的衡量工具。微信号没法直接点开链接,所以点击“阅读原文”可以进到工具页面。(我真的不是在做广告)
这里写图片描述
这里写图片描述
这里我们利用“MONO内存对象深度分析”的特点。该功能可以允许用户抓取某一时刻的Mono内存状态,并且提供不同时刻内存状态的比较,快速定位到新增的内存分配。

鉴于Cube官方已经给出了详细的使用说明,就不再赘述数据的抓取过程。这里简单聊一下如何通过Cube抓取的数据更好地追踪和解决问题。

如下图所示,假设我们已经抓取了两次数据(snapshot1 & snapshot2),并且进行比较,得到两次内存快照之间新增的分配数据。

这里写图片描述

比较之后得到如下图所示的一系列数据,总结来说,就是在某个堆栈,分配了某个类型的对象,占用xx内存。这样的数据会有成千上万条(上文所说,代码中的内存分配,是非常细碎,并且数量极多的,在这里得到了验证),并且其中有很多堆栈是重复的,因为每一次的内存分配(即使是同一处位置产生的分配),都会产生一条记录。无序的数据影响了我们对数据的处理,这里我们对数据做一些分析整理。

这里写图片描述

我们举一些简单的例子来说明处理的过程。

每一条记录,都是经过一系列的函数调用(堆栈),最终分配了一些内存,用图形化的方式表示为:

这里写图片描述
让我们多加一些数据:

这里写图片描述

通过对图的观察,我们发现可以把上述离散的图整理成一棵树:

这里写图片描述

将所有数据都做同样的归类处理之后,可以得到一棵或多棵这样的分配树。这么做的好处是:
1) 根据函数,可以将内存的分配做一个模块的划分,快速定位到相关的模块。
2) 可以清晰地看到每一层函数的分配总量(如A函数总共分配4096+20+4096B),可以根据占用内存的多少决定修复的优先级。
将对比之后的新增项一一清理之后,就可以基本清除Mono内存的多余分配和泄漏了。

顺藤摸瓜——从Mono中寻找资源引用

在尝试寻找资源引用,修复资源泄露之前,我们需要先了解一下如何在Unity中定位资源泄漏。
我们需要使用Unity自带的Memory Profiler(注意不是上文说的Unity5的新Profiler,是老的残疾版Profiler)。举个简单的例子,在Unity编辑器环境下运行游戏工程,经过“大厅”页面,进入到“单局”。此时打开Unity Profiler,切换到Memory并做一次内存采样(具体请参考https://docs.unity3d.com/Manual/ProfilerMemory.html,不赘述)。 在采样的结果中(其中包含采样时刻内存中所有的资源),点开Assets->Texture2D,如果其中可以看到有“大厅”UI使用的贴图(如下图),那么我们可以定义这张UI贴图,属于资源上的泄漏。

这里写图片描述
为什么说这种情况就属于资源泄漏呢,因为这张UI贴图,是在“大厅”时申请的,但是在“单局”时,它已经不被需要了,可是它还在内存中。这种在不需要的时候,却还存在的内存占用,就是上文我们定义的内存泄漏。

那么在平时项目中,我们如何找到这些泄漏的资源呢?
最直观的方法,当然也是最笨的方法,就是在每次游戏状态切换的时候,做一次内存采样,并且将内存中的资源一一点开查看,判断它是否是当前游戏状态真正需要的。这种方法最大的问题,就是耗时耗力,资源数量太多眼睛容易看花看漏。

这里介绍两种讨巧的方法:
1) 通过资源名来识别。即在美术资源(如贴图、材质)命名的时候,就将其所属的游戏状态放在文件名中,如某贴图叫做BG.png,在大厅中使用,则修改为OG_BG.png(OG = OutGame)。这样在一坨IG(IG=InGame)资源里面,混入了一个OG,可以很容易地识别出来,也方便利用程序来识别。这么做还有一个好处,可以强化美术对资源生命周期的认识,在制作资源,特别是规划UI图集时,可以有一个指导意义。
2) 通过Unity提供的接口Resources.FindObjectsOfTypeAll()进行资源的Dump,可以根据需求Dump贴图、材质、模型或其他资源类型,只需要将Type作为参数传入即可。Dump成功之后我们将结果保存成一份文本文件,这样可以用Beyond Compare对多次Dump之后的结果进行比较,找到新增的资源,那么这些资源就是潜在的泄漏对象,需要重点追查。
结合上述的方法与思路,应该可以轻松找到泄漏的资源了。

此时我们再回头看一下Unity Profiler,其实Unity提供了资源索引的查找功能,只不过该功能是以一个树形结构的文本来展示的(如下图)。上文曾提到过,Unity内部的引用关系往往是非常复杂的,可能需要通过十几甚至几十层的引用,才能找到最终的引用者,并且引用关系错综复杂,形成一张庞大的图,此时光靠展开树形结构来查找,几乎是不可能的事了。

这里写图片描述

防微杜渐,避免内存泄漏

介绍完对于Unity内存泄漏的追踪方法,我还想往下多讲一步,只要我们在平时开发的过程多做思考,防微杜渐,内存泄漏是完全可以避免的。相对于等泄漏发生了再回头来追查,平时多花点时间清理“垃圾”反而是更加高效的做法。
落地到平时的开发流程中,在这里提出几点建议,欢迎各位大牛补充:
1) 在架构上,多添加析构的abstract接口,提醒团队成员,要注意清理自己产生的“垃圾”。
2) 严格控制static的使用,非必要的地方禁止使用static。
3) 强化生命周期的概念,无论是代码对象还是资源,都有它存在的生命周期,在生命周期结束后就要被释放。如果可能,需要在功能设计文档中对生命周期加以描述。
相信大家出门旅游,都有看过下图类似的标语,作为一名合格的程序猿,也应该能够处理好代码中的“垃圾”,不要让我们的游戏成为一个“垃圾场”。

为了避免以上手游性能方面对游戏的负面影响,腾讯WeTest平台下的Cube工具可以帮助开发者发现游戏内分类资源的一个占用情况,帮助在游戏开发过程中不断改善玩家的体验。目前功能还在免费开放中。点击http://wetest.qq.com/cube/立即体验!

作者:wetest_tencent 发表于2016/8/25 21:34:02 原文链接
阅读:38 评论:0 查看评论

MTK6580(Android6.0)-使用DTS注册平台设备、匹配平台驱动

$
0
0


一、初始化device tree
file:kernel-3.18/init/main.c
asmlinkage __visible void __init start_kernel(void)
{
	...
	setup_arch(&command_line);
	...
}
file:kernel-3.18/arch/arm64/kernel/setup.c
void __init setup_arch(char **cmdline_p)
{	
	...


	unflatten_device_tree();
	...
}
文件:kernel-3.18/drivers/of/fdt.c
/**
 * unflatten_device_tree - create tree of device_nodes from flat blob
 *
 * unflattens the device-tree passed by the firmware, creating the
 * tree of struct device_node. It also fills the "name" and "type"
 * pointers of the nodes so the normal device-tree walking functions
 * can be used.
 */
void __init unflatten_device_tree(void)
{
	__unflatten_device_tree(initial_boot_params, &of_allnodes,
				early_init_dt_alloc_memory_arch);


	/* Get pointer to "/chosen" and "/aliases" nodes for use everywhere */
	of_alias_scan(early_init_dt_alloc_memory_arch);
}
其中of_allnodes的类型为struct device_node ,各成员解析如下:
struct device_node {
    const char *name;----------------------device node name
    const char *type;-----------------------对应device_type的属性
    phandle phandle;-----------------------对应该节点的phandle属性
    const char *full_name; ----------------从“/”开始的,表示该node的full path
   struct property *properties;-------------该节点的属性列表
    struct property *deadprops; ----------如果需要,删除某些属性,并挂入到deadprops的列表
    struct device_node *parent;------parent、child以及sibling将所有的device node连接起来
    struct device_node *child;
    struct device_node *sibling;
    struct device_node *next; --------通过该指针可以获取相同类型的下一个node
    struct device_node *allnext;-------通过该指针可以获取node global list下一个node
    struct proc_dir_entry *pde;--------开放到userspace的proc接口信息
    struct kref kref;-------------该node的reference count
    unsigned long _flags;
    void *data;
};
file:kernel-3.18/drivers/of/fdt.c
/**
 * __unflatten_device_tree - create tree of device_nodes from flat blob
 *
 * unflattens a device-tree, creating the
 * tree of struct device_node. It also fills the "name" and "type"
 * pointers of the nodes so the normal device-tree walking functions
 * can be used.
 * @blob: The blob to expand
 * @mynodes: The device_node tree created by the call
 * @dt_alloc: An allocator that provides a virtual address to memory
 * for the resulting tree
 */
static void __unflatten_device_tree(void *blob,
			     struct device_node **mynodes, //mynodes 为全局链表
			     void * (*dt_alloc)(u64 size, u64 align))
{
	unsigned long size;
	int start;
	void *mem;
	struct device_node **allnextp = mynodes;


	pr_debug(" -> unflatten_device_tree()\n");


	if (!blob) {
		pr_debug("No device tree pointer\n");
		return;
	}


	pr_debug("Unflattening device tree:\n");
	pr_debug("magic: %08x\n", fdt_magic(blob));
	pr_debug("size: %08x\n", fdt_totalsize(blob));
	pr_debug("version: %08x\n", fdt_version(blob));


	if (fdt_check_header(blob)) {
		pr_err("Invalid device tree blob header\n");
		return;
	}


	/* First pass, scan for size */
	start = 0;
	size = (unsigned long)unflatten_dt_node(blob, NULL, &start, NULL, NULL, 0);
	size = ALIGN(size, 4);


	pr_debug("  size is %lx, allocating...\n", size);


	/* Allocate memory for the expanded device tree */
	mem = dt_alloc(size + 4, __alignof__(struct device_node));
	memset(mem, 0, size);


	*(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);


	pr_debug("  unflattening %p...\n", mem);


	/* Second pass, do actual unflattening */
	start = 0;
	unflatten_dt_node(blob, mem, &start, NULL, &allnextp, 0);
	if (be32_to_cpup(mem + size) != 0xdeadbeef)
		pr_warning("End of tree marker overwritten: %08x\n",
			   be32_to_cpup(mem + size));
	*allnextp = NULL;


	pr_debug(" <- unflatten_device_tree()\n");
}
/**
 * unflatten_dt_node - Alloc and populate a device_node from the flat tree
 * @blob: The parent device tree blob
 * @mem: Memory chunk to use for allocating device nodes and properties
 * @p: pointer to node in flat tree
 * @dad: Parent struct device_node
 * @allnextpp: pointer to ->allnext from last allocated device_node
 * @fpsize: Size of the node path up at the current depth.
 */
static void * unflatten_dt_node(void *blob,
				void *mem,
				int *poffset,
				struct device_node *dad,
				struct device_node ***allnextpp,
				unsigned long fpsize)
{
	const __be32 *p;
	struct device_node *np;
	struct property *pp, **prev_pp = NULL;
	const char *pathp;
	unsigned int l, allocl;
	static int depth = 0;
	int old_depth;
	int offset;
	int has_name = 0;
	int new_format = 0;


	pathp = fdt_get_name(blob, *poffset, &l);
	if (!pathp)
		return mem;


	allocl = l++;


	/* version 0x10 has a more compact unit name here instead of the full
	 * path. we accumulate the full path size using "fpsize", we'll rebuild
	 * it later. We detect this because the first character of the name is
	 * not '/'.
	 */
	if ((*pathp) != '/') {
		new_format = 1;
		if (fpsize == 0) {
			/* root node: special case. fpsize accounts for path
			 * plus terminating zero. root node only has '/', so
			 * fpsize should be 2, but we want to avoid the first
			 * level nodes to have two '/' so we use fpsize 1 here
			 */
			fpsize = 1;
			allocl = 2;
			l = 1;
			pathp = "";
		} else {
			/* account for '/' and path size minus terminal 0
			 * already in 'l'
			 */
			fpsize += l;
			allocl = fpsize;
		}
	}


	np = unflatten_dt_alloc(&mem, sizeof(struct device_node) + allocl,
				__alignof__(struct device_node));
	if (allnextpp) {
		char *fn;
		of_node_init(np);
		np->full_name = fn = ((char *)np) + sizeof(*np);
		if (new_format) {
			/* rebuild full path for new format */
			if (dad && dad->parent) {
				strcpy(fn, dad->full_name);
#ifdef DEBUG
				if ((strlen(fn) + l + 1) != allocl) {
					pr_debug("%s: p: %d, l: %d, a: %d\n",
						pathp, (int)strlen(fn),
						l, allocl);
				}
#endif
				fn += strlen(fn);
			}
			*(fn++) = '/';
		}
		memcpy(fn, pathp, l);


		prev_pp = &np->properties;
		**allnextpp = np;
		*allnextpp = &np->allnext;
		if (dad != NULL) {
			np->parent = dad;
			/* we temporarily use the next field as `last_child'*/
			if (dad->next == NULL)
				dad->child = np;
			else
				dad->next->sibling = np;
			dad->next = np;
		}
	}
	/* process properties */
	for (offset = fdt_first_property_offset(blob, *poffset);
	     (offset >= 0);
	     (offset = fdt_next_property_offset(blob, offset))) {
		const char *pname;
		u32 sz;


		if (!(p = fdt_getprop_by_offset(blob, offset, &pname, &sz))) {
			offset = -FDT_ERR_INTERNAL;
			break;
		}


		if (pname == NULL) {
			pr_info("Can't find property name in list !\n");
			break;
		}
		if (strcmp(pname, "name") == 0)
			has_name = 1;
		pp = unflatten_dt_alloc(&mem, sizeof(struct property),
					__alignof__(struct property));
		if (allnextpp) {
			/* We accept flattened tree phandles either in
			 * ePAPR-style "phandle" properties, or the
			 * legacy "linux,phandle" properties.  If both
			 * appear and have different values, things
			 * will get weird.  Don't do that. */
			if ((strcmp(pname, "phandle") == 0) ||
			    (strcmp(pname, "linux,phandle") == 0)) {
				if (np->phandle == 0)
					np->phandle = be32_to_cpup(p);
			}
			/* And we process the "ibm,phandle" property
			 * used in pSeries dynamic device tree
			 * stuff */
			if (strcmp(pname, "ibm,phandle") == 0)
				np->phandle = be32_to_cpup(p);
			pp->name = (char *)pname;
			pp->length = sz;
			pp->value = (__be32 *)p;
			*prev_pp = pp;
			prev_pp = &pp->next;
		}
	}
	/* with version 0x10 we may not have the name property, recreate
	 * it here from the unit name if absent
	 */
	if (!has_name) {
		const char *p1 = pathp, *ps = pathp, *pa = NULL;
		int sz;


		while (*p1) {
			if ((*p1) == '@')
				pa = p1;
			if ((*p1) == '/')
				ps = p1 + 1;
			p1++;
		}
		if (pa < ps)
			pa = p1;
		sz = (pa - ps) + 1;
		pp = unflatten_dt_alloc(&mem, sizeof(struct property) + sz,
					__alignof__(struct property));
		if (allnextpp) {
			pp->name = "name";
			pp->length = sz;
			pp->value = pp + 1;
			*prev_pp = pp;
			prev_pp = &pp->next;
			memcpy(pp->value, ps, sz - 1);
			((char *)pp->value)[sz - 1] = 0;
			pr_debug("fixed up name for %s -> %s\n", pathp,
				(char *)pp->value);
		}
	}
	if (allnextpp) {
		*prev_pp = NULL;
		np->name = of_get_property(np, "name", NULL);
		np->type = of_get_property(np, "device_type", NULL);


		if (!np->name)
			np->name = "<NULL>";
		if (!np->type)
			np->type = "<NULL>";
	}


	old_depth = depth;
	*poffset = fdt_next_node(blob, *poffset, &depth);
	if (depth < 0)
		depth = 0;
	while (*poffset > 0 && depth > old_depth)
		mem = unflatten_dt_node(blob, mem, poffset, np, allnextpp,
					fpsize);


	if (*poffset < 0 && *poffset != -FDT_ERR_NOTFOUND)
		pr_err("unflatten: error %d processing FDT\n", *poffset);


	return mem;
}
二、具体创建platform device的过程

file:kernel-3.18/arch/arm64/kernel/setup.c

//系统调用of_platform_populate

static int __init arm64_device_init(void)
{
	of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL);
	return 0;
}
arch_initcall_sync(arm64_device_init); //arch_initcall_sync为宏函数,这里可以理解为module的加载(和module_init类似)
file:kernel-3.18/drivers/of/platform.c
/**
 * of_platform_populate() - Populate platform_devices from device tree data
 * @root: parent of the first level to probe or NULL for the root of the tree
 * @matches: match table, NULL to use the default
 * @lookup: auxdata table for matching id and platform_data with device nodes
 * @parent: parent to hook devices from, NULL for toplevel
 *
 * Similar to of_platform_bus_probe(), this function walks the device tree
 * and creates devices from nodes.  It differs in that it follows the modern
 * convention of requiring all device nodes to have a 'compatible' property,
 * and it is suitable for creating devices which are children of the root
 * node (of_platform_bus_probe will only create children of the root which
 * are selected by the @matches argument).
 *
 * New board support should be using this function instead of
 * of_platform_bus_probe().
 *
 * Returns 0 on success, < 0 on failure.
 */
int of_platform_populate(struct device_node *root,
			const struct of_device_id *matches,
			const struct of_dev_auxdata *lookup,
			struct device *parent)
{
	struct device_node *child;
	int rc = 0;


	root = root ? of_node_get(root) : of_find_node_by_path("/");
	if (!root)
		return -EINVAL;


	for_each_child_of_node(root, child) {
		rc = of_platform_bus_create(child, matches, lookup, parent, true);
		if (rc)
			break;
	}


	of_node_put(root);
	return rc;
}
file:kernel-3.18/drivers/of/base.c
static struct device_node *__of_find_node_by_path(struct device_node *parent,
						const char *path)
{
	struct device_node *child;
	int len = strchrnul(path, '/') - path;


	if (!len)
		return NULL;


	__for_each_child_of_node(parent, child) {
		const char *name = strrchr(child->full_name, '/');
		if (WARN(!name, "malformed device_node %s\n", child->full_name))
			continue;
		name++;
		if (strncmp(path, name, len) == 0 && (strlen(name) == len))
			return child;
	}
	return NULL;
}

       在这个函数中有一个很关键的全局变量:allnodes,它的定义是在 drivers/of/base.c 里面,struct device_node *allnodes;这应该所就是那个所谓的“device tree data”
了。它应该指向了 device tree 的根节点。我们知道 device tree 是由 DTC(Device Tree Compiler)编译成二进制文件DTB(Ddevice Tree Blob)的,然后在系统上电之
后由bootloader加载到内存中去,这个时候还没有device tree,而在内存中只有一个所谓的 DTB,这只是一个以某个内存地址开始的一堆原始的dt数据,没有树结构。kernel
的任务需要把这些数据转换成一个树结构然后再把这棵树的根节点的地址赋值给allnodes就行了。这个过程一定是非常重要,因为没有这个 device tree 那所有的设备就没办
法初始化,所以这个 dt 树的形成一定在kernel 刚刚启动的时候就完成了。

/**
 * of_platform_bus_create() - Create a device for a node and its children.
 * @bus: device node of the bus to instantiate
 * @matches: match table for bus nodes
 * @lookup: auxdata table for matching id and platform_data with device nodes
 * @parent: parent for new device, or NULL for top level.
 * @strict: require compatible property
 *
 * Creates a platform_device for the provided device_node, and optionally
 * recursively create devices for all the child nodes.
 */
static int of_platform_bus_create(struct device_node *bus,
				  const struct of_device_id *matches,
				  const struct of_dev_auxdata *lookup,
				  struct device *parent, bool strict)
{
	const struct of_dev_auxdata *auxdata;
	struct device_node *child;
	struct platform_device *dev;
	const char *bus_id = NULL;
	void *platform_data = NULL;
	int rc = 0;


	/* Make sure it has a compatible property */
	if (strict && (!of_get_property(bus, "compatible", NULL))) { //没有compatible直接返回
		pr_debug("%s() - skipping %s, no compatible prop\n", 
			 __func__, bus->full_name);
		return 0;
	}


	auxdata = of_dev_lookup(lookup, bus); //在传入lookup table寻找和该device node匹配的附加数据 
	if (auxdata) {
		bus_id = auxdata->name;
		platform_data = auxdata->platform_data;
	}


	if (of_device_is_compatible(bus, "arm,primecell")) {
		/*
		 * Don't return an error here to keep compatibility with older
		 * device tree files.
		 */
		of_amba_device_create(bus, bus_id, platform_data, parent);
		return 0;
	}
    /*这个函数是真正生成struct device的地方*/
	dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);
	if (!dev || !of_match_node(matches, bus))
		return 0;
		
 /*如果compatible属性不是"simple-bus"和"arm,amba-bus"则在返回,不继续遍历子节点。
 这里我的理解是"simple-bus"和"arm,amba-bus"这两种设备不具备热插拔能力,因此在这里就先创建了struct device*/
 
	for_each_child_of_node(bus, child) {
		pr_debug("   create child: %s\n", child->full_name);
		rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict);
		if (rc) {
			of_node_put(child);
			break;
		}
	}
	of_node_set_flag(bus, OF_POPULATED_BUS);
	return rc;
}


/**
 * of_platform_device_create_pdata - Alloc, initialize and register an of_device
 * @np: pointer to node to create device for
 * @bus_id: name to assign device
 * @platform_data: pointer to populate platform_data pointer with
 * @parent: Linux device model parent device.
 *
 * Returns pointer to created platform device, or NULL if a device was not
 * registered.  Unavailable devices will not get registered.
 */
static struct platform_device *of_platform_device_create_pdata(
					struct device_node *np,
					const char *bus_id,
					void *platform_data,
					struct device *parent)
{
	struct platform_device *dev;


	if (!of_device_is_available(np) ||
	    of_node_test_and_set_flag(np, OF_POPULATED))
		return NULL;
/* of_device_alloc除了分配struct platform_device的内存,还分配了该platform device需要的resource的内存
   (参考struct platform_device 中的resource成员)。当然,这就需要解析该device node的interrupt资源以及
    memory address资源,这些资源的原始数据都来自dtb中。*/
	dev = of_device_alloc(np, bus_id, parent);
	if (!dev)
		goto err_clear_flag;


	of_dma_configure(&dev->dev);
	dev->dev.bus = &platform_bus_type;  //设置匹配方式
	dev->dev.platform_data = platform_data;


	/* We do not fill the DMA ops for platform devices by default.
	 * This is currently the responsibility of the platform code
	 * to do such, possibly using a device notifier
	 */


	if (of_device_add(dev) != 0) {  ////把这个device加入到设备模型中,后续驱动注册的时候就可以匹配到了
		platform_device_put(dev);   
		goto err_clear_flag;
	}


	return dev;


err_clear_flag:
	of_node_clear_flag(np, OF_POPULATED);
	return NULL;
}
file:kernel-3.18/drivers/of/device.c
int of_device_add(struct platform_device *ofdev)
{
	BUG_ON(ofdev->dev.of_node == NULL);


	/* name and id have to be set so that the platform bus doesn't get
	 * confused on matching */
	ofdev->name = dev_name(&ofdev->dev);
	ofdev->id = -1;


	/* device_add will assume that this device is on the same node as
	 * the parent. If there is no parent defined, set the node
	 * explicitly */
	if (!ofdev->dev.parent)
		set_dev_node(&ofdev->dev, of_node_to_nid(ofdev->dev.of_node));


	return device_add(&ofdev->dev);
}
file:kernel-3.18/drivers/base/core.c
/**
 * device_add - add device to device hierarchy.
 * @dev: device.
 *
 * This is part 2 of device_register(), though may be called
 * separately _iff_ device_initialize() has been called separately.
 *
 * This adds @dev to the kobject hierarchy via kobject_add(), adds it
 * to the global and sibling lists for the device, then
 * adds it to the other relevant subsystems of the driver model.
 *
 * Do not call this routine or device_register() more than once for
 * any device structure.  The driver model core is not designed to work
 * with devices that get unregistered and then spring back to life.
 * (Among other things, it's very hard to guarantee that all references
 * to the previous incarnation of @dev have been dropped.)  Allocate
 * and register a fresh new struct device instead.
 *
 * NOTE: _Never_ directly free @dev after calling this function, even
 * if it returned an error! Always use put_device() to give up your
 * reference instead.
 */
int device_add(struct device *dev)
{
	struct device *parent = NULL;
	struct kobject *kobj;
	struct class_interface *class_intf;
	int error = -EINVAL;


	dev = get_device(dev);
	if (!dev)
		goto done;


	if (!dev->p) {
		error = device_private_init(dev);
		if (error)
			goto done;
	}


	/*
	 * for statically allocated devices, which should all be converted
	 * some day, we need to initialize the name. We prevent reading back
	 * the name, and force the use of dev_name()
	 */
	if (dev->init_name) {
		dev_set_name(dev, "%s", dev->init_name);
		dev->init_name = NULL;
	}


	/* subsystems can specify simple device enumeration */
	if (!dev_name(dev) && dev->bus && dev->bus->dev_name)
		dev_set_name(dev, "%s%u", dev->bus->dev_name, dev->id);


	if (!dev_name(dev)) {
		error = -EINVAL;
		goto name_error;
	}


	pr_debug("device: '%s': %s\n", dev_name(dev), __func__);


	parent = get_device(dev->parent);
	kobj = get_device_parent(dev, parent);
	if (kobj)
		dev->kobj.parent = kobj;


	/* use parent numa_node */
	if (parent)
		set_dev_node(dev, dev_to_node(parent));


	/* first, register with generic layer. */
	/* we require the name to be set before, and pass NULL */
	error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);
	if (error)
		goto Error;


	/* notify platform of device entry */
	if (platform_notify)
		platform_notify(dev);


	error = device_create_file(dev, &dev_attr_uevent);
	if (error)
		goto attrError;


	if (MAJOR(dev->devt)) {
		error = device_create_file(dev, &dev_attr_dev);
		if (error)
			goto ueventattrError;


		error = device_create_sys_dev_entry(dev);
		if (error)
			goto devtattrError;


		devtmpfs_create_node(dev);
	}


	error = device_add_class_symlinks(dev);
	if (error)
		goto SymlinkError;
	error = device_add_attrs(dev);
	if (error)
		goto AttrsError;
	error = bus_add_device(dev);
	if (error)
		goto BusError;
	error = dpm_sysfs_add(dev);
	if (error)
		goto DPMError;
	device_pm_add(dev);


	/* Notify clients of device addition.  This call must come
	 * after dpm_sysfs_add() and before kobject_uevent().
	 */
	if (dev->bus)
		blocking_notifier_call_chain(&dev->bus->p->bus_notifier,
					     BUS_NOTIFY_ADD_DEVICE, dev);


	kobject_uevent(&dev->kobj, KOBJ_ADD);
	bus_probe_device(dev); //------------very important----------------
	if (parent)
		klist_add_tail(&dev->p->knode_parent,
			       &parent->p->klist_children);


	if (dev->class) {
		mutex_lock(&dev->class->p->mutex);
		/* tie the class to the device */
		klist_add_tail(&dev->knode_class,
			       &dev->class->p->klist_devices);


		/* notify any interfaces that the device is here */
		list_for_each_entry(class_intf,
				    &dev->class->p->interfaces, node)
			if (class_intf->add_dev)
				class_intf->add_dev(dev, class_intf);
		mutex_unlock(&dev->class->p->mutex);
	}
done:
	put_device(dev);
	return error;
 DPMError:
	bus_remove_device(dev);
 BusError:
	device_remove_attrs(dev);
 AttrsError:
	device_remove_class_symlinks(dev);
 SymlinkError:
	if (MAJOR(dev->devt))
		devtmpfs_delete_node(dev);
	if (MAJOR(dev->devt))
		device_remove_sys_dev_entry(dev);
 devtattrError:
	if (MAJOR(dev->devt))
		device_remove_file(dev, &dev_attr_dev);
 ueventattrError:
	device_remove_file(dev, &dev_attr_uevent);
 attrError:
	kobject_uevent(&dev->kobj, KOBJ_REMOVE);
	kobject_del(&dev->kobj);
 Error:
	cleanup_device_parent(dev);
	if (parent)
		put_device(parent);
name_error:
	kfree(dev->p);
	dev->p = NULL;
	goto done;
}
EXPORT_SYMBOL_GPL(device_add);
file:kernel-3.18/drivers/base/bus.c
/**
 * bus_add_device - add device to bus
 * @dev: device being added
 *
 * - Add device's bus attributes.
 * - Create links to device's bus.
 * - Add the device to its bus's list of devices.
 */
int bus_add_device(struct device *dev)
{
	struct bus_type *bus = bus_get(dev->bus);
	int error = 0;


	if (bus) {
		pr_debug("bus: '%s': add device %s\n", bus->name, dev_name(dev));
		error = device_add_attrs(bus, dev);
		if (error)
			goto out_put;
		error = device_add_groups(dev, bus->dev_groups);
		if (error)
			goto out_id;
		error = sysfs_create_link(&bus->p->devices_kset->kobj,
						&dev->kobj, dev_name(dev));
		if (error)
			goto out_groups;
		error = sysfs_create_link(&dev->kobj,
				&dev->bus->p->subsys.kobj, "subsystem");
		if (error)
			goto out_subsys;
		klist_add_tail(&dev->p->knode_bus, &bus->p->klist_devices);
	}
	return 0;


out_subsys:
	sysfs_remove_link(&bus->p->devices_kset->kobj, dev_name(dev));
out_groups:
	device_remove_groups(dev, bus->dev_groups);
out_id:
	device_remove_attrs(bus, dev);
out_put:
	bus_put(dev->bus);
	return error;
}
file:kernel-3.18/drivers/base/core.c
int device_add_groups(struct device *dev, const struct attribute_group **groups)
{
	return sysfs_create_groups(&dev->kobj, groups);
}
三、匹配平台驱动
file:kernel-3.18/drivers/base/bus.c
/**
 * bus_probe_device - probe drivers for a new device
 * @dev: device to probe
 *
 * - Automatically probe for a driver if the bus allows it.
 */
void bus_probe_device(struct device *dev)
{
	struct bus_type *bus = dev->bus;
	struct subsys_interface *sif;
	int ret;


	if (!bus)
		return;


	if (bus->p->drivers_autoprobe) {
		ret = device_attach(dev);
		WARN_ON(ret < 0);
	}


	mutex_lock(&bus->p->mutex);
	list_for_each_entry(sif, &bus->p->interfaces, node)
		if (sif->add_dev)
			sif->add_dev(dev, sif);
	mutex_unlock(&bus->p->mutex);
}
file:kernel-3.18/drivers/base/dd.c
/**
 * device_attach - try to attach device to a driver.
 * @dev: device.
 *
 * Walk the list of drivers that the bus has and call
 * driver_probe_device() for each pair. If a compatible
 * pair is found, break out and return.
 *
 * Returns 1 if the device was bound to a driver;
 * 0 if no matching driver was found;
 * -ENODEV if the device is not registered.
 *
 * When called for a USB interface, @dev->parent lock must be held.
 */
int device_attach(struct device *dev)
{
	int ret = 0;


	device_lock(dev);
	if (dev->driver) {
		if (klist_node_attached(&dev->p->knode_driver)) {
			ret = 1;
			goto out_unlock;
		}
		ret = device_bind_driver(dev);
		if (ret == 0)
			ret = 1;
		else {
			dev->driver = NULL;
			ret = 0;
		}
	} else {
		ret = bus_for_each_drv(dev->bus, NULL, dev, __device_attach);
		pm_request_idle(dev);
	}
out_unlock:
	device_unlock(dev);
	return ret;
}
<span style="font-size:14px;">file<span style="color:#009900;">:kernel-3.18/drivers/base/dd.c</span></span>
static int __device_attach(struct device_driver *drv, void *data)
{
	struct device *dev = data;


	if (!driver_match_device(drv, dev))
		return 0;


	return driver_probe_device(drv, dev);
}
file:kernel-3.18/drivers/base/base.h
static inline int driver_match_device(struct device_driver *drv,
				      struct device *dev)
{
	return drv->bus->match ? drv->bus->match(dev, drv) : 1;
}
file:kernel-3.18/drivers/base/dd.c
/**
 * driver_probe_device - attempt to bind device & driver together
 * @drv: driver to bind a device to
 * @dev: device to try to bind to the driver
 *
 * This function returns -ENODEV if the device is not registered,
 * 1 if the device is bound successfully and 0 otherwise.
 *
 * This function must be called with @dev lock held.  When called for a
 * USB interface, @dev->parent lock must be held as well.
 */
int driver_probe_device(struct device_driver *drv, struct device *dev)
{
	int ret = 0;


	if (!device_is_registered(dev))
		return -ENODEV;


	pr_debug("bus: '%s': %s: matched device %s with driver %s\n",
		 drv->bus->name, __func__, dev_name(dev), drv->name);


	pm_runtime_barrier(dev);
	ret = really_probe(dev, drv);
	pm_request_idle(dev);


	return ret;
}
static int really_probe(struct device *dev, struct device_driver *drv)
{
	int ret = 0;
	int local_trigger_count = atomic_read(&deferred_trigger_count);


	atomic_inc(&probe_count);
	pr_debug("bus: '%s': %s: probing driver %s with device %s\n",
		 drv->bus->name, __func__, drv->name, dev_name(dev));
	WARN_ON(!list_empty(&dev->devres_head));


	dev->driver = drv;


	/* If using pinctrl, bind pins now before probing */
	ret = pinctrl_bind_pins(dev);
	if (ret)
		goto probe_failed;


	if (driver_sysfs_add(dev)) {
		printk(KERN_ERR "%s: driver_sysfs_add(%s) failed\n",
			__func__, dev_name(dev));
		goto probe_failed;
	}


	if (dev->bus->probe) {
		ret = dev->bus->probe(dev);
		if (ret)
			goto probe_failed;
	} else if (drv->probe) {
		ret = drv->probe(dev);
		if (ret)
			goto probe_failed;
	}


	driver_bound(dev);
	ret = 1;
	pr_debug("bus: '%s': %s: bound device %s to driver %s\n",
		 drv->bus->name, __func__, dev_name(dev), drv->name);
	goto done;


probe_failed:
	devres_release_all(dev);
	driver_sysfs_remove(dev);
	dev->driver = NULL;
	dev_set_drvdata(dev, NULL);


	if (ret == -EPROBE_DEFER) {
		/* Driver requested deferred probing */
		dev_info(dev, "Driver %s requests probe deferral\n", drv->name);
		driver_deferred_probe_add(dev);
		/* Did a trigger occur while probing? Need to re-trigger if yes */
		if (local_trigger_count != atomic_read(&deferred_trigger_count))
			driver_deferred_probe_trigger();
	} else if (ret != -ENODEV && ret != -ENXIO) {
		/* driver matched but the probe failed */
		printk(KERN_WARNING
		       "%s: probe of %s failed with error %d\n",
		       drv->name, dev_name(dev), ret);
	} else {
		pr_debug("%s: probe of %s rejects match %d\n",
		       drv->name, dev_name(dev), ret);
	}
	/*
	 * Ignore errors returned by ->probe so that the next driver can try
	 * its luck.
	 */
	ret = 0;
done:
	atomic_dec(&probe_count);
	wake_up(&probe_waitqueue);
	return ret;
}
在platform_driver 的注册中,将drv->driver.probe赋值为platform_drv_probe
file:kernel-3.18/drivers/base/platform.c
/**
 * __platform_driver_register - register a driver for platform-level devices
 * @drv: platform driver structure
 * @owner: owning module/driver
 */
int __platform_driver_register(struct platform_driver *drv,
				struct module *owner)
{
	drv->driver.owner = owner;
	drv->driver.bus = &platform_bus_type;
	if (drv->probe)
		drv->driver.probe = platform_drv_probe;
	if (drv->remove)
		drv->driver.remove = platform_drv_remove;
	if (drv->shutdown)
		drv->driver.shutdown = platform_drv_shutdown;


	return driver_register(&drv->driver);
}
在platform_drv_probe 中调用drv->probe(dev)也就是platform_driver的probe,到此平台的驱动和设备的匹配已经完成
static int platform_drv_probe(struct device *_dev)
{
	struct platform_driver *drv = to_platform_driver(_dev->driver);
	struct platform_device *dev = to_platform_device(_dev);
	int ret;


	ret = of_clk_set_defaults(_dev->of_node, false);
	if (ret < 0)
		return ret;


	ret = dev_pm_domain_attach(_dev, true);
	if (ret != -EPROBE_DEFER) {
		ret = drv->probe(dev);
		if (ret)
			dev_pm_domain_detach(_dev, true);
	}


	if (drv->prevent_deferred_probe && ret == -EPROBE_DEFER) {
		dev_warn(_dev, "probe deferral not supported\n");
		ret = -ENXIO;
	}


	return ret;
}
四、总结
    其实引入dts机制后,总线设备如platform bus,i2c bus 的注册,直接从从dts中解析设备资源,之后注册设备流程和先前设备注册大同小异,而驱动注册和加载以及设备、
驱动的匹配和先前没有区别。也就是说,Linux核心思想设备、驱动、总线关系没有改变,改变只是一小部分实现形式而已。dts在新版内核已经完全支持,对于我们普通的
内核驱动开发人员来说,只需要了解其简单的dts节点规则, 设备驱动匹配规则,能够熟练的从dts中解析数据等(当然这不是本章内容)。
ps:dts 的注册平台设备和匹配平台驱动的流程和很简单,具体实现细节十分麻烦,例如它用C语言进行的许多解析操作,涉及到繁琐算法,本篇不作分析。

作者:xiaopangzi313 发表于2016/8/26 22:16:43 原文链接
阅读:16 评论:0 查看评论

Qt之保存/恢复窗口的几何形状

$
0
0

简述

本节介绍如何使用geometry属性保存和恢复窗口的几何形状。在Windows中,基本上是存储QWindow::geometry()的结果,并在下次会话调用show()之前,调用QWindow::setGeometry()。

在X11中,这可能无法工作,因为一个不可见的窗口没有边框。后来窗口管理器将装饰窗口。当这种情况发生时,窗口朝向屏幕的底部/右下角移动取决于装饰框的大小。虽然X提供了一种方法来避免这种转变,有些窗口管理器仍无法实现此功能。

详细描述

当使用Qt Widgets时,Qt提供了保存和恢复一个窗口部件的几何形状和状态的函数。QWidget::saveGeometry()保存窗口的尺寸和最大化/全屏状态,而QWidget::restoreGeometry()用来恢复它。恢复函数还检查恢复几何形状是否超出可用的屏幕几何形状,如果超过了,则会适当地进行修改。

保存/恢复几何形状的方式有两种:

  • 保存/恢复geometry()
  • 保存/恢复pos()、size()

保存/恢复geometry()

保存几何形状

一般情况下,在程序退出之前,保存最后一次的几何形状和位置。

void MyMainWindow::closeEvent(QCloseEvent *event)
{
    QSettings settings("MyCompany", "MyApp");
    settings.setValue("geometry", saveGeometry());
    settings.setValue("windowState", saveState());
    QMainWindow::closeEvent(event);
}

恢复几何形状

在show()之前,然后读取上次保存的信息,一般在构造函数中调用。

void MainWindow::readSettings()
{
    QSettings settings("MyCompany", "MyApp");
    restoreGeometry(settings.value("myWidget/geometry").toByteArray());
    restoreState(settings.value("myWidget/windowState").toByteArray());
}

另一种方式是同时存储pos()和size(),并在show()之前调用QWidget::resize()和move() 。

保存/恢复pos()、size()

保存几何形状

一般情况下,在程序退出之前,保存最后一次的几何形状和位置。常在closeEvent()中调用。

void MainWindow::writeSettings()
{
    QSettings settings(QCoreApplication::organizationName(), QCoreApplication::applicationName());
    settings.setValue("geometry", saveGeometry());
}

恢复几何形状

在show()之前,然后读取上次保存的信息,一般在构造函数中调用。

void MainWindow::readSettings()
{
    QSettings settings(QCoreApplication::organizationName(), QCoreApplication::applicationName());
    const QByteArray geometry = settings.value("geometry", QByteArray()).toByteArray();
    if (geometry.isEmpty()) {
        const QRect availableGeometry = QApplication::desktop()->availableGeometry(this);
        resize(availableGeometry.width() / 3, availableGeometry.height() / 2);
        move((availableGeometry.width() - width()) / 2,
               (availableGeometry.height() - height()) / 2);
    } else {
          restoreGeometry(geometry);
    }
}
作者:u011012932 发表于2016/9/2 22:04:45 原文链接
阅读:70 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>