iOS开发-位运算

前言

从现代计算机电路来说,只有高电平/低电平两种状态,即为0/1状态,计算机中所有的数据按照具体的编码格式以二进制的形式存储在设备中。


直接操作这些二进制数据的位数据就是位运算,在iOS中基本所有的位运算都通过枚举声明传值的方式将位运算的实现细节隐藏了起来:

typedef NS_OPTIONS(NSUInteger, UIRectEdge) {
    UIRectEdgeNone   = 0,
    UIRectEdgeTop    = 1 << 0,
    UIRectEdgeLeft   = 1 << 1,
    UIRectEdgeBottom = 1 << 2,
    UIRectEdgeRight  = 1 << 3,
    UIRectEdgeAll    = UIRectEdgeTop | UIRectEdgeLeft | UIRectEdgeBottom | UIRectEdgeRight
} NS_ENUM_AVAILABLE_IOS(7_0);

位运算是一种极为高效乃至可以说最为高效的计算方式,虽然现代程序开发中编译器已经为我们做了大量的优化,但是合理的使用位运算可以提高代码的可读性以及执行效率。

基础计算

在了解怎么使用位运算之前,笔者简单说一下CPU处理计算的过程。如果你对CPU的计算方式有所了解,可以跳过这一节。

当代码int sum = 11 + 79被执行的时候,计算机直接将两个数的二进制位进行相加和进位操作:

11:  0 0 0 0 1 0 1 1
79:  0 1 0 0 1 1 1 1
————————————————————
90:  0 1 0 1 1 0 1 0

通常来说CPU执行两个数相加操作所花费的时间被我们称作一个时钟周期,而2.0GHz频率的CPU表示可以在一秒执行运算2.0*1024*1024*1024个时钟周期。相较于加法运算,下面看一下11*211*4的二进制结果:

11:  0 0 0 0 1 0 1 1  *  2
————————————————————
22:  0 0 0 1 0 1 1 0

11:  0 0 0 0 1 0 1 1  *  4
————————————————————
44:  0 0 1 0 1 1 0 0

简单来说,不难发现当某个数乘以2的N次幂的时候,结果等同于将这个数的二进制位置向左移动N位,在代码中我们使用num << N表示将num的二进制数据左移N个位置,其效果等同于下面这段代码:

for (int idx = 0; idx < N; idx++) {
    num *= 2;
}

假如相乘的两个数都不是2的N次幂,这时候编译器会将其中某个值分解成多个2的N次幂相加的结果进行运算。比如37 * 69,这时候CPU会将37分解成32+4+1,然后换算成(69<<5) + (69<<2) + (69<<0)的方式计算出结果。因此,计算两个数相乘通常需要十个左右的时钟周期。 同理,代码num >> N的作用等效于:

for (int idx = 0; idx < N; idx++) {
    num /= 2;
}

但是两个数相除花费的时钟周期要比乘法还要多得多,其大部分消耗在将数值分解成多个2的N次幂上。除此之外,浮点数涉及到的计算更为复杂,这里也简单聊聊浮点数的准确度问题。拿float类型来说,总共使用了32bit的存储空间,其中第一位表示正负,2~13位表示整数部分的值,14~32位之中分别存储了小数位以及科学计数的标识值(这里可能并不那么准确,主要是为了给读者一个大概的介绍)。由于小数位的二进制数据依旧保持2的N次幂特性,假如下面的二进制属于小数位:

1 0 1 1 1 0 0 1

那么这部分小数位的值等于:1/2 + 1/4 + 1/8 + 1/16 + 1/128 = 0.9453125。因此,当你把一个没有任何规律的小数例如3.1415926535898存入计算机的时候,小数点后面会被拆解成很多的2的N次幂进行保存。由于小数位总是有限的,因此当分解的N超出这些位数时导致存储不下,就会出现精度偏差。另一方面,这样的分解计算势必要消耗大量的时钟周期,这也是大量的浮点数运算(cell动态计算)容易引发卡顿的原因。所以,当小数位过多时,改用字符串存储是一个更优的选择。

位运算符

使用的运算符包括下面:

| 含义 | 运算符 |
| -----|:----:| ----:|
| 左移 | << |
| 右移 | >> |
| 按位或 | ︳ |
| 按位并 | & |
| 按位取反 | ~ |
| 按位异或 | ^ |

  • & 操作
    0 0 1 0 1 1 1 0 46
    1 0 0 1 1 1 0 1 157
    ———————————————
    0 0 0 0 1 1 0 0 12

  • | 操作
    0 0 1 0 1 1 1 0 46
    1 0 0 1 1 1 0 1 157
    ———————————————
    1 0 1 1 1 1 1 1 191

  • ~ 操作
    0 0 1 0 1 1 1 0 46
    ———————————————
    1 1 0 1 0 0 0 1 209

  • ^ 操作
    0 0 1 0 1 1 1 0 46
    1 0 0 1 1 1 0 1 157
    ———————————————
    1 0 1 1 0 0 1 1 179

色彩存储

使用位运算包括下面几个原因:
1、代码更简洁
2、更高的效率
3、更少的内存

简单来说,我们如何单纯的保存一张RGB色彩空间下的图片?由于图片由一系列的像素组成,每个像素有着自己表达的颜色,因此需要这么一个类用来表示图片的单个像素:

@interface Pixel

@property (nonatomic, assign) CGFloat red;
@property (nonatomic, assign) CGFloat green;
@property (nonatomic, assign) CGFloat blue;
@property (nonatomic, assign) CGFloat alpha;

@end

那么在4.7寸的屏幕上,启动图需要750*1334个这样的类,不计算其他数据,单单是变量的存储需要750*1334*4*8 = 32016000个字节的占用内存。但实际上我们使用到的图片总是将RGBA这四个属性保存在一个int类型或者其它相似的少字节变量中。

由于色彩取值范围为0~255,即2^1 ~ 2^8-1不超过一个字节的整数占用内存。因此可以通过左移运算保证每一个字节只存储了一个决定色彩的值:

- (int)rgbNumberWithRed: (int)red green: (int)green blue: (int)blue alpha: (float)alpha {
    int bitPerByte = 8;
    int maxNumber = 255;

    int alphaInt = alpha * maxNumber;
    int rgbNumber = (red << (bitPerByte*3)) + (green << (bitPerByte*2)) + (blue << bitPerByte) + alphaInt;
}

同理,通过右移操作保证数值的最后一个字节存储着需要的数据,并用0xff将值取出来:

- (void)obtainRGBA: (int)rgbNumber {
    int mask = 0xff;
    int bitPerByte = 8;

    double alphaInt = (rgbNumber & mask) / 255.0;
    int blue = ((rgbNumber >> bitPerByte) & mask);
    int green = ((rgbNumber >> (bitPerByte*2)) & mask);
    int red = ((rgbNumber >> (bitPerByte*3)) & mask);
}

对比使用类和位运算存储,效率跟内存占用上可以说是完败。

位运算应用

苹果在类对象的结构中使用了位运算这一设计:每个对象都有一个整型类型的标识符flags,其中多个不同的位表示了是否存在弱引用、是否被初始化、对象是否为常量等信息,对于这些存储的数据通过&|等运算符获取出来。这些在runtime源码中都能看到,下面是一段伪代码(参数请勿对号入座)

#define IS_TAGGED_POINTER (1 << 12);
#define HAS_WEAK_REFERENCE (1 << 13);

inline void objc_object::free() {
    if (this->flags | HAS_WEAK_REFERENCE) {
        ///  set all weak reference point to nil
    }
}

inline int objc_object::retainCount() {
    if (this.flags | IS_TAGGED_POINTER) {
        return (int)INT_MAX;
    } else {
        return this->retainCount;
    }
}

......

借鉴苹果的运算操作,可以声明一个应用常用权限的枚举,来获取我们的应用权限:

typedef NS_ENUM(NSInteger, LXDAuthorizationType)
{
    LXDAuthorizationTypeNone = 0,
    LXDAuthorizationTypePush = 1 << 0,  ///<    推送授权
    LXDAuthorizationTypeLocation = 1 << 1,  ///<    定位授权
    LXDAuthorizationTypeCamera = 1 << 2,    ///<    相机授权
    LXDAuthorizationTypePhoto = 1 << 3,     ///<    相册授权
    LXDAuthorizationTypeAudio = 1 << 4,  ///<    麦克风授权
    LXDAuthorizationTypeContacts = 1 << 5,  ///<    通讯录授权
};

通过声明一个全局的权限变量来保存不同的授权信息。当应用拥有对应的授权时,通过|操作符保证对应的二进制位的值被修改成1。否则对对应授权枚举进行~取反后再&操作消除二进制位的授权表达。为了完成这些工作,建立一个工具类来获取以及更新授权的状态:

/*!
 *  @brief  获取应用授权信息工具,最低使用版本:iOS8.0
 */
NS_CLASS_AVAILABLE_IOS(8_0) @interface LXDAuthObtainTool : NSObject

/// 获取当前应用权限
+ (LXDAuthorizationType)obtainAuthorization;
/// 更新应用权限
+ (void)updateAuthorization;

@end


#pragma mark -  LXDAuthObtainTool.m
static LXDAuthorizationType kAuthorization;

@implementation LXDAuthObtainTool

+ (void)initialize
{
    kAuthorization = LXDAuthorizationTypeNone;
    [self updateAuthorization];
}

/// 获取当前应用权限
+ (LXDAuthorizationType)obtainAuthorization
{
    return kAuthorization;
}

/// 更新应用权限
+ (void)updateAuthorization
{
    /// 推送
    if ([UIApplication sharedApplication].currentUserNotificationSettings.types == UIUserNotificationTypeNone) {
        kAuthorization &= (~LXDAuthorizationTypePush);
    } else {
        kAuthorization |= LXDAuthorizationTypePush;
    }
    /// 定位
    if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedAlways || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedWhenInUse) {
        kAuthorization |= LXDAuthorizationTypeLocation;
    } else {
        kAuthorization &= (~LXDAuthorizationTypeLocation);
    }
    /// 相机
    if ([AVCaptureDevice authorizationStatusForMediaType: AVMediaTypeVideo] == AVAuthorizationStatusAuthorized) {
        kAuthorization |= LXDAuthorizationTypeCamera;
    } else {
        kAuthorization &= (~LXDAuthorizationTypeCamera);
    }
    /// 相册
    if ([PHPhotoLibrary authorizationStatus] == PHAuthorizationStatusAuthorized) {
        kAuthorization |= LXDAuthorizationTypePhoto;
    } else {
        kAuthorization &= (~LXDAuthorizationTypePhoto);
    }
    /// 麦克风
    [[AVAudioSession sharedInstance] requestRecordPermission: ^(BOOL granted) {
        if (granted) {
            kAuthorization |= LXDAuthorizationTypeAudio;
        } else {
            kAuthorization &= (~LXDAuthorizationTypeAudio);
        }
    }];
    /// 通讯录
    if ([UIDevice currentDevice].systemVersion.doubleValue >= 9) {
        if ([CNContactStore authorizationStatusForEntityType: CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) {
            kAuthorization |= LXDAuthorizationTypeContacts;
        } else {
            kAuthorization &= (~LXDAuthorizationTypeContacts);
        }
    } else {
        if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized) {
            kAuthorization |= LXDAuthorizationTypeContacts;
        } else {
            kAuthorization &= (~LXDAuthorizationTypeContacts);
        }
    }
}

@end

在我们需要使用某些授权的时候,例如打开相册时,直接使用&运算符判断权限即可:

- (void)openCamera {
    LXDAuthorizationType type = [LXDAuthObtainTool obtainAuthorization];
    if (type & LXDAuthorizationTypeCamera) {
        ///  open camera
    } else {
        /// alert
    }
}

在数据存储的方面位运算拥有着占用内存少,高效率的优点,当然位运算能做的不仅仅是这些,比如笔者项目有这样的一个需求:用户登录成功之后在首页界面请求服务器下载所有金额相关的数据。这个需求最大的问题是:

AFN2.3+版本的请求库不支持同步请求,当需要多个请求任务一次性执行时,判断请求任务完成是很麻烦的一件事情。

由于NSInteger拥有8个字节64位的二进制位,因此笔者将每一个二进制位用来表示单个任务请求的完成状态。已知登陆后需要同步数据的接口为N(<64)个,因此可以声明一个全部请求任务完成后的状态变量:

NSInteger complete = 0;
for (int idx = 0; idx < N; idx++) {
    complete |= (1 << idx);
}

然后使用一个标志变量flags用来记录当前任务请求的完成情况,每一个数据同步的任务完成之后对应的二进制位就置为1

__block NSInteger flags = 0;
NSArray<NSString *> * urls = @[......];
NSArray<NSDictionary *> * params = @[......];    

for (NSInteger idx = 0; idx < urls.count; idx++) {
    NSString * url = urls[idx];
    NSDictionary * param = params[idx];

    [LXDDataSyncTool syncWithUrl: url params: param complete: ^{
        flags |= (1 << idx);
        if ( (flags ^ complete) == 0 ) {
            [self completeDataSync];
        }
    }];
}

位运算与算法

在普遍使用高级语言开发的大环境下,位运算的实现更多的被封装起来,因此大多数开发者在项目开发中不见得会使用这一机制。在上面基础计算一节中笔者说过两个数相加只需要一个时钟周期(虽然CPU从寄存器读取存放数据也需要额外的时钟周期,但通常这部分的花销总是常量级,可以忽略不计)

由于位运算的处理基本也在一个时钟周期完成,位运算这一操作备受算法封装者的喜爱。比如交换两个变量的值一般情况下代码是:

int sum = a;
a = b;
b = sum;

又或者:

a = a + b;
b = a - b;
a = a - b;

如果通过位运算的方式则不需要任何加减操作或者临时变量:

a ^= b;
b = a ^ b;
a = a ^ b;

上面的代码和第二种方式的实现思路类似,都是将ab合并成单个变量,再分别消除变量中的ab的值(^运算会对相同二进制位的值置0,意味着b^b的结果等于0)

进阶题:找出整型数组中唯一的单独数字,数组中的其他数字的个数为2个

通过上面不用中间变量交换ab的值可以得出下面的最简代码:

- (int)singleDog(int * nums, int length) {
    int singleDog = 0;
    for (int idx = 0; idx < length; idx++) {
        singleDog ^= nums[idx];
    }
    return singleDog;
}

位运算与密码

说了这么多,上面对于位运算的使用都是在编程的环境下,能不能让位运算有更多的使用意义呢?答案是肯定的。俗话说道高一尺,魔高一丈。近日来互联网公司的数据被攻破已经不算什么大新闻了,对于喜欢共用同一个账号名的用户(例如笔者)来说,被撞库的风险非常的大,使用位运算可以帮助我们生成不同的但是又不难记的密码。简单来说,我们可以基于规律的运算,传入随机性的变量生成非唯一的密码。例如:

const NSInteger kRemoveBit = 8;
extern NSString * const kEncryptKey;
static inline NSString * kPasswordGenerate(NSString * randomString) {
    NSInteger hash = randomString.hash;
    NSString * encryptString = [NSString stringWithFormat: @"%lu", hash ^ kEncryptKey.hash];
    return [NSString stringWithFormat: @"%@%lu%@", randomString, ((hash << kRemoveBit) | (hash >> kRemoveBit)), encryptString];
}

传入的string就是生成密码的随机变量,通过获取hash进行少量的位运算之后,由于hash过程的不可逆属性以及在保证string足够长的情况下,基本可以保证密码的复杂性以及安全性。另外,传入的randomString可以根据不同网址生成,例如简书主页为jianshu.com,可以用以下的方式生成:

static inline NSString * kRandomString(NSString * string) {
    unichar ch = [string characterAtIndex: 0];
    return [NSString stringWithFormat: @"%@%lu", string, (ch | string.hash)];
}
NSString * random = kRandomString(@"jianshu.com");
NSString * password = kPasswordGenerate(random);

复杂的密码可以有效的保护账户安全,但是同样复杂的密码更难以记住,更别说我们需要不同账户对应不同的复杂密码。位运算+hash的结合运用可以帮助我们来管理这些复杂的密码,唯一的缺点大概就是手机要装着这么一个app

关注iOS开发获得笔者更新动态
转载请注明地址以及作者

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容