前言
经常看到苹果代码在传值时,使用按位或(|)来传值多个参数值
比如:
self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight |
UIViewAutoresizingFlexibleTopMargin;
其实是通过按位或(|)、按位与(&)运算来实现的
假如定义一个枚举值:
typedef enum {
OptionsOne = 1, // 二进制形式0b0001
OptionsTwo = 2, // 二进制形式0b0010
OptionsThree = 4, // 二进制形式0b0100
OptionsFour = 8 // 二进制形式0b1000
} Options;
注意看二进制形式的表现形式,1都是占据不同的位数
将Options的One、Two和Four几个枚举值进行或运算,来看下结果:
// 或运算的结果是,只要位数有一个1,结果就是1
0b0001
|0b0010
|0b1000
------
0b1011 // 或运算的结果就是0b1011
由于1占据不同的位数,所以定义枚举值也可以这样写:
typedef enum {
OptionsOne = 1<<0, // 1向左移动0位:0b0001
OptionsTwo = 1<<1, // 1向左移动1位:0b0010
OptionsThree = 1<<2, // 1向左移动2位:0b0100
OptionsFour = 1<<3 // 1向左移动3位:0b1000
} Options;
这样就可以在定义枚举值的时候,不用写具体的实现值,以避免写错
假如我们有个方法需要传Options的值
[self setOptions: OptionsOne | OptionsFour];
// 这个或运算的结果是0b1001,本质上等同于:
[self setOptions: OptionsOne + OptionsFour];
怎么让方法知道调用者是传了哪些值进来呢?这时候需要进行按位与(&)运算了
// 按位与运算的结果是只要位数都是1,结果就是1,否则就是0
0b1011
&0b0101
-------
0b0001
所以方法接收传值的时候:
- (void)setOptions:(Options)options
{
if (options & 0b0001) {
NSLog(@"包含了OptionsOne");
}
if (options & 0b0010) {
NSLog(@"包含了OptionsTwo");
}
if (options & 0b0100) {
NSLog(@"包含了OptionsThree");
}
if (options & 0b1000) {
NSLog(@"包含了OptionsFour");
}
}
由于定义枚举的时候,枚举值就是具体的偏移值,所以也可以这样写:
- (void)setOptions:(Options)options
{
if (options & OptionsOne) {
NSLog(@"包含了OptionsOne");
}
if (options & OptionsTwo) {
NSLog(@"包含了OptionsTwo");
}
if (options & OptionsThree) {
NSLog(@"包含了OptionsThree");
}
if (options & OptionsFour) {
NSLog(@"包含了OptionsFour");
}
}
内存空间的节省
假设我们在编写一个底层库的时候(底层库在设计时需要尽可能的节省内存,提升程序性能),Model里面有三个属性,都是BOOL类型,正常写法都是这样:
@property (assign, nonatomic) BOOL pro1;
@property (assign, nonatomic) BOOL pro2;
@property (assign, nonatomic) BOOL pro3;
那么这样,苹果就会生成三个成员变量,分别是:
{
BOOL _pro1;
BOOL _pro2;
BOOL _pro3;
}
三个BOOL类型的成员变量会占用3个字节的内存空间;
BOOL类型的属性在用的时候,要么是0,要么是1,既然是这样,我们何不用二进制位来存储这些值呢?三个属性分别占用三个二进制位就行了,三个合起来只需要占用一个字节就行了。
既然要实现以上需求,在定义Model属性的时候,就不能够用属性来表达了
.h文件需要自己来实现Model的get方法和set方法:
- (void)setPro1:(BOOL)pro1;
- (void)setPro2:(BOOL)pro2;
- (void)setPro3:(BOOL)pro3;
- (BOOL)isPro1;
- (BOOL)isPro2;
- (BOOL)isPro3;
.m
文件来实现get和set功能:
// 定义掩码,每个属性占据不同的二进制位
#define Pro1Mask (1<<0)
#define Pro2Mask (1<<1)
#define Pro3Mask (1<<2)
@interface MyModel()
{
// 二进制一个字节表达: 0b 0000 0000
char _pros;
}
@end
@implementation MyModel
- (instancetype)init
{
if (self = [super init]) {
_pros = 0b00000000;
}
return self;
}
- (void)setPro1:(BOOL)pro1
{
if (pro1) {
_pros |= Pro1Mask;
} else {
// ~是取反的意思
_pros &= ~Pro1Mask;
}
}
- (BOOL)isPro1
{
// 按位与出来的结果是一位的,但BOOL类型是八位的; 如果不进行一下转换,直接返回出去结果会出问题,
// 两个!!(非)来将值强制改为bool类型,只要不是0,结果都是YES
return !!(_pros & Pro1Mask);
}
- (void)setPro2:(BOOL)pro2
{
if (pro2) {
_pros |= Pro2Mask;
} else {
_pros &= ~Pro2Mask;
}
}
- (BOOL)isPro2
{
return !!(_pros & Pro2Mask);
}
- (void)setPro3:(BOOL)pro3
{
if (pro3) {
_pros |= Pro3Mask;
} else {
_pros &= ~Pro3Mask;
}
}
- (BOOL)isPro3
{
return !!(_pros & Pro3Mask);
}
异或
首先异或表示当两个数的二进制表示,进行异或运算时,当前位的两个二进制表示不同则为1相同则为0.该方法被广泛推广用来统计一个数的1的位数!
BOOL match = a ^ b;
如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0
比较
或(|),与(&),异或(^) 之间的区别:
异或(^)不同则为1,相同则为0 | 或(|)只要位数有一个1,结果就是1,否则就是0 | 与(&) 只要位数都是1,结果就是1,否则就是0 |
---|---|---|
0^0=0 | 0|0=0 | 0&0=0 |
1^0=1 | 1|0=1 | 1&0=0 |
0^1=1 | 0|1=1 | 0&1=0 |
1^1=0 | 1|1=1 | 1&1=1 |
结构体位域
如上面已用按位或按位与实现一个字节存储不同的属性值,节省了内存空间,但写法还是略微复杂,有没有更简单的方法呢?
结构体是支持位域技术的,可以这样写:
struct {
char one : 1; // 代表只占一位空间
char two : 1; // 代表只占一位空间
char three : 1; // 代表只占一位空间
} mystruct;
one
将会在地址值位数的最后边,three
在地址值位数的最前面(先写的在地址值的最右边)
这样写就代表三个只占一个字节,那么这个结构体也只占用一个字节
我们把上面的代码稍稍改动一下,用结构体来实现上面的功能
.m
文件:
@interface MyModel()
{
// 用结构体的位域技术
struct {
char Pro1Mask : 1;
char Pro2Mask : 1;
char Pro3Mask : 1;
} _pros;
}
@end
@implementation MyModel
- (void)setPro1:(BOOL)pro1
{
_pros.Pro1Mask = pro1;
}
- (BOOL)isPro1
{
return !!_pros.Pro1Mask;
}
- (void)setPro2:(BOOL)pro2
{
_pros.Pro2Mask = pro2;
}
- (BOOL)isPro2
{
return !!_pros.Pro2Mask;
}
- (void)setPro3:(BOOL)pro3
{
_pros.Pro3Mask = pro3;
}
- (BOOL)isPro3
{
return !!_pros.Pro3Mask;
}
如上,结构体位域功能同样可以实现功能,但写法上更加简单
共用体
普通写法的结构体:
struct date {
int year;
int month;
int day;
};
以上结构体,year,month,day
是独立存在的,各自内存空间互不影响
但共用体就不一样了
共用体表示共同、合并占用内存空间;可以定义多个成员,共用体的大小由最大的成员的大小决定。对某一个成员赋值,会覆盖其他成员的值(因为他们共享一块内存。但前提是成员所占字节数相同,当成员所占字节数不同时只会覆盖相应字节上的值,比如对char成员赋值就不会把整个int成员覆盖掉,因为char只占一个字节,而int占四个字节)
// 定义共用体
union date {
int year; // 4个字节
int month;
int day;
};
// 共用体使用
union date d;
d.year = 1011;
// 这里打印将会输出1011,因为取month时,就是在整个共用体结构里面取,在4个字节里面取结果是一样的。
NSLog(@"%d", d.month);
这种写法,在内存上也是一个字节,本质上等同于声明 char *_pros:
union {
char bits;
struct {
char pro1 : 1;
char pro2 : 1;
char pro3 : 1;
};
} _pros;
IP地址的转换
通常情况下,IPV4的地址我们都使用一个String类型的变量来接收,但String占用的空间相比int类型来说比较大大,在java中它占用40 + 2 * n(n为字符串长度)
java
当中空String字符串所占用的空间是: 对象头(8 字节)+ 引用 (4 字节 ) + char 数组(16 字节)+ 1个 int(4字节)+ 1个long(8字节)= 40 字节
这时候可以考虑将这个IP地址转换为int类型来存储(时间换空间的方式)
IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)。
IP地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。 例:点分十进IP地址(100.4.5.6),实际上是32位二进制数(01100100.00000100.00000101.00000110)
也就是说,ip 地址本身就是一个32位的二进制数,只是通常被以 a.b.c.d 的形式表示而已。原理在这,解决也就容易得很了
/**
* 将 ip 字符串转换为 int 类型的数字
* <p>
* 思路就是将 ip 的每一段数字转为 8 位二进制数,并将它们放在结果的适当位置上
*
* @param ipString ip字符串,如 127.0.0.1
* @return ip字符串对应的 int 值
*/
public static int ip2Int(String ipString) {
// 取 ip 的各段
String[] ipSlices = ipString.split("\\.");
int rs = 0;
for (int i = 0; i < ipSlices.length; i++) {
// 将 ip 的每一段解析为 int,并根据位置左移 8 位
int intSlice = Integer.parseInt(ipSlices[i]) << 8 * i;
// 求与
rs = rs | intSlice;
}
return rs;
}
将每段的 int 值左移到恰当的位置后跟保存结果的 int 值进行或运算。
以 255.255.255.255 这个地址为例,上面的或运算过程如下
00000000 00000000 00000000 00000000
00000000 00000000 00000000 11111111
------------或运算------------
00000000 00000000 00000000 11111111
00000000 00000000 11111111 00000000
------------或运算------------
00000000 00000000 11111111 11111111
00000000 11111111 00000000 00000000
------------或运算------------
00000000 11111111 11111111 11111111
11111111 00000000 00000000 00000000
-----------最终结果------------
11111111 11111111 11111111 11111111
int
转换为 ip 字符串
/**
* 将 int 转换为 ip 字符串
*
* @param ipInt 用 int 表示的 ip 值
* @return ip字符串,如 127.0.0.1
*/
public static String int2Ip(int ipInt) {
String[] ipString = new String[4];
for (int i = 0; i < 4; i++) {
// 每 8 位为一段,这里取当前要处理的最高位的位置
int pos = i * 8;
// 取当前处理的 ip 段的值
int and = ipInt & (255 << pos);
// 将当前 ip 段转换为 0 ~ 255 的数字,注意这里必须使用无符号右移
ipString[i] = String.valueOf(and >>> pos);
}
return String.join(".", ipString);
}
本文首次发布于 孙忠良 Blog, 作者 [@sunzhongliang] , 转载请保留原文链接.