咱們都知道,計算機中的全部數據最終都是以二進制(bit)的形式存儲在計算機的。而在咱們平時開發中所接觸數據的大可能是字節爲單位的,有了位運算以後咱們就能夠操做字節中的比特位了。在iOS的runtime源碼以及NS_OPTIONS 中都運用了位運算的知識,可見其重要性了。另外值得一提的是,大部分語言的位運算符都相同,因此這是一篇老小皆宜的文章。html
閱讀本篇文章前,你須要知道的一些東西:c++
bool c = 0xff == 0b11111111; // true
bool c0 = 0b11111111 == 0377; // true
bool c1 = 0377 == 255; //true
複製代碼
位運算符就像是控制比特位的扳手,在學習位運算前先介紹下每一個運算符的意義及其用法。
程序員
運算規則:只有在兩個值都爲1時結果才爲1,兩個值中有一個爲0結果便爲0。在編程語言裏通常用 & 表示與運算符。編程
舉個例子,10100001 & 10001001 = 10000001。(注:操做數都爲二進制。)
緩存
運算規則: 兩個值中有一個爲1結果便爲1,兩個值都爲0時結果才爲0。在編程語言裏通常用 | 表示或運算符。網絡
舉個例子,10100001 | 10001001 = 10101001。架構
運算規則: 只有當兩個值不相同時結果才爲1,兩個值相同時結果爲0。在編程語言裏通常用 ^ 表示異或運算符。編程語言
舉個例子,10100001 ^ 10001001 = 00101000。
ide
在數值的二進制表示方式上,將0變爲1,將1變爲0。在編程語言裏通常用 ~ 表示取反運算符。
來看一個例子可能會更加直觀:函數
右移將操做數的二進制位總體向右移動N位,空出部分用0填充,在編程語言裏通常用 >>表示右移運算符。
舉個例子,下圖對二進制 10111101 右移3位後,最右邊的101被移除,左邊空出來3位用0填充(本文章默認全部數據都爲無符號類型,因此本操做爲邏輯右移)。
左移將操做數的二進制位總體向左移動N位,空出部分用0填充,在編程語言裏通常用 << 表示左移運算符。
舉個例子,下圖對二進制 10111101 左移4位後,最左邊的1011被移除,右邊空出來4位用0填充。
這裏先介紹一些比較簡單實用的位運算技巧,這些技巧在平常開發中也是比較經常使用的。
假設x=0b10011010,如今我想將第五、6位置爲1,將其變爲0b11111010,那麼執行 (x = x | 0b01100000) 就是咱們想要的結果;那如果想將第0、五、6爲置爲1,變成0b11111011呢?那麼執行(x = x | 0b01100001)就是咱們想要的結果。 根據上面的兩個例子,咱們能夠獲得一個結論:
掩碼這個詞常常能在計算機網絡裏聽到,比較熟悉的話就是子網掩碼了。掩碼是起的很是好的一個名字,當咱們的操做數和掩碼進行與運算(&)後,掩碼中二進制爲0的位會屏蔽掉原操做數對應的二進制位。 舉個例子,假如如今我有一個2個字節的數據0xBA15,若要屏蔽掉中間0xA1這8位二進制變成0xB005,該如何設計掩碼呢?答案很簡單,只要將掩碼中間8位設爲0其餘設爲1便可,因此本例中的掩碼應爲0xF00F,0xBA15 & 0xF00F=0xB005。能夠結合下圖理解:
這個函數傳入一個data,返回其二進制從右邊開始數第i位的值。
unsigned char getBit( unsigned long data , int i ) {
// i = 0時,表明取最右邊的哪一位。
data = data >> i ;
return data & 1 ;
}
複製代碼
原理很簡單,先將data右移i位,這樣能保證第i位的值處於data的最右邊,而後再用data & 1取出便可。 舉個例子,若是我調用了{getBit(168,3)}
,168對應的二進制爲10101000,右移3位後變成00010101,最後00010101 & 00000001 = 1,取出成功。
筆者在mac上的unsigned long 是8個字節,能夠存儲64位二進制,因爲沒有符號位,故只需將這64位二進制都填充爲1就獲得unsigned long變量的最大值了。
// 將全0取反變爲全1裝進變量x中。 unsigned long x = ~0; // 輸出二進制爲全1的變量x printf("unsigned long max = %lu\n",x);複製代碼
若是最後一位二進制爲0,那麼這個數字就是偶數,若是爲1就是奇數。這裏給出實現函數:
int isOdd(int value) {
return (value & 1); // 取出最後一位二進制,若爲1則是奇數
}
複製代碼
說下大體原理:若最後一位二進制爲0,那麼二進制轉成十進制後必然能夠寫成2n的形式,必爲偶數。好比我隨便寫一個最後一位爲0的二進制數字 10001010,那麼其十進制數爲2+2^3+2^7 = 2*(1+2^2+2^6)
,故爲偶數,你們能夠多寫幾組數字驗證。
關於負數:雖然負數在計算機中以補碼的方式存儲,但因爲補碼最後一位和原碼最後一位相同,因此上面的函數一樣適用於負數。爲何呢?舉個例子:
看到了吧,最後又變回去了。
int x =0xba;//10111010
int count = 0;
while (x!=0) {
x = x&(x-1);
count++;
}
printf("%d\n",count);
複製代碼
循環中每次執行x = x&(x-1)後,x的二進制的最後一個1就會被消去。當全部1都被消去後,count計數完畢,x=0,退出循環。
那麼爲何x = x&(x-1)可以消去其二進制的最後一個1呢?舉個例子:
能夠發現規律:
// 注:參數是c++的引用類型。
void swap(int &a,int &b) {
a=a^b;
b=a^b;
a=a^b;
}
複製代碼
想要了解原理,須要先知道幾個異或運算的性質:
假設一開始,a=k,b=t
。
a=a^b
後 a=k^t
;b=a^b
後 b = k^t^t=k
,注意這裏用到了自反性;a=a^b
後 a=k^t^k=t^k^k=t
,注意這裏用到了交換律和自反性;a=t,b=k
,交換完成。固然了,不只限於交換整型變量。舉一個不太經常使用的例子,咱們能夠不用臨時變量交換兩個c語言字符串。下面代碼中的a和b本質上是在交換"a-a-a-a-a-a"和"b-b-b-b-b-b"地址,因此效果也是同樣的。
char *a = "a-a-a-a-a-a";// 存儲在數據區的字符串常量
char *b = "b-b-b-b-b-b";//存儲在數據區的字符串常量
printf("before exchange: a=%s,b=%s\n",a,b);
a = (char*)((long)a^(long)b);
b = (char*)((long)a^(long)b);
a = (char*)((long)a^(long)b);
printf("after exchange: a=%s,b=%s\n",a,b);
/* 最終輸出爲: /* 最終輸出爲: before exchange: a=a-a-a-a-a-a,b=b-b-b-b-b-b after exchange: a=b-b-b-b-b-b,b=a-a-a-a-a-a */
複製代碼
假設如今有一集合A={a,b,c},要求生成這個集合的全部子集。構造子集時,咱們可使用二進制中第i位的值決定是否要選取集合A中的第i個元素。其中值爲1表明選取,值爲0表明不選取。舉個例子,100表明只選第一個元素a,其構成的子集爲{a};101表明選取第一個a以及第三個c,其構成的子集爲{a,c}。
下面列舉出A的全部子集:
編號 | A的子集 | 人類思考過程 | 二進制表示 |
---|---|---|---|
0 | {} | 什麼都不選 | 000 |
1 | {c} | 不選a,不選b,選c | 001 |
2 | {b} | 不選a,選b,不選c | 010 |
3 | {b,c} | 不選a,選b,選c | 011 |
4 | {a} | 選a,不選b,不選c | 100 |
5 | {a,c} | 選a,不選b,選c | 101 |
6 | {a,b} | 選a,選b,不選c | 110 |
7 | {a,b,c} | 選a,選b,選c | 111 |
細心的話,應該能發現上面的表格中的編號和二進制剛恰好能對的上。因此對於有個n個元素的集合,只要生成0到2^n-1個整數編號,而後根據每一個編號對應的二進制解析出相應的子集便可。 下面是c語言實現的代碼:
#include <stdio.h>
#include <string.h>
void prinstSubSet(char *S,int id) {
int n = (int)strlen(S);// 集合的元素個數
char result[100];
int index=0;
printf("{");
for(int i=n-1;i>=0;i--){
if ((id>>i)&1) {
// 若第i位值爲1,表明選擇第i個元素(從右邊開始數)
result[index++]=S[n-1-i];//因爲字符串第0個字符是最左邊,因此要顛倒下。
}
}
for(int i =0;i<index;i++) {
printf("%c",result[i]);
if(i!=index-1)
printf(",");
}
printf("}\n");
}
void create(char *S) {
int n = (int)strlen(S);// 集合的元素個數
int begin = 0;
int end = (1<<n)-1; // 2^n-1
//生成0到2^n-1個編號(id)
for (int id = begin;id<=end;id++) {
prinstSubSet(S, id);// 根據編號對應的二進制輸出子集
}
}
int main(int argc, const char * argv[]) {
create("abc");// 生成{a,b,c}的子集
return 0;
}
複製代碼
這裏介紹C語言程序設計這本書中的兩個很是實用的函數,相信你在平時的項目中也能應用的到。
該函數用來返回x中從右邊數第p位開始向右數n位二進制。
unsigned getBits(unsigned x,int p,int n) {
return (x>>(p+1-n)) & ~(~0<<n);
}
複製代碼
舉個例子,調用getBits(168,5,3)
後,返回168對應二進制10101000從右邊數第5位開始向右3位二進制,也就是101。能夠結合下圖理解:
一開始執行(x>>(p+1-n))
這裏是爲了將指望得到的字段移動到最右邊。用上面的例子,執行完後x變成:
~(~0<<n)
是爲了生成右邊n位全1的掩碼。 對於上面的例子~(~0<<3)
,咱們一塊兒來分析下過程。
(x>>(p+1-n))
和~(~0<<n)
與運算,取出指望字段。對於上面的例子,對應過程圖以下:
該函數返回x中從第p位開始的n個(二進制)位設置爲y中最右邊n位的值,x的其他各位保持不變。
unsigned setBits(unsigned x, int p,int n , unsigned y) {
return ( x & ~( ~( ~0 << n ) << ( p+1-n ) ) ) |
(y & ~(~0 << n) ) << (p+1-n);
}
複製代碼
舉個例子(#2),調用setbits(168, 5, 4, 0b0101)
後,將168對應二進制10101000從右邊數第5位開始的4個二進制位設置爲0101,設置完後變成10010100,最後將結果返回。能夠結合下圖理解:
第一眼看到這個函數代碼仍是有一些恐怖的,不用懼怕,咱們一層層解析,相信你必定能感覺位運算的精妙之處!
咱們先要將函數拆成兩個部分看待,第一部分是( x & ~( ~( ~0 << n ) << ( p+1-n ) ) )
記爲$0;另外一部分是(y & ~(~0 << n) ) << (p+1-n)
記爲$1。 下面分析下$0和$1的做用:
下面具體分析下$0是如何將指望修改的n個二進制清0的:
~( ~( ~0 << n ) << ( p+1-n ) )
生成x清0所須要的掩碼。~(~0 << n)
生成右邊n個1,其他全爲0的。代入例子(#2)的參數,也就是~(~0 << 4)
,結果爲:00001111。這裏爲了方便記憶,把~(~0 << n)
記爲$$0 。$$0 << (p+1-n)
,將右邊n個1左移到相應位置上。代入例子(#2)的參數及上一步的結果,應執行00001111 << (5+1-4)
,結果爲00111100。這裏將$$0 << (p+1-n)
記爲$$1。~$$1
,生成清零所需的掩碼。代入例子(#2)的參數及上一步的結果,應執行~00111100
,結果爲11000011,掩碼生成完畢。x & ~$$1
,用掩碼將x中待清零的位清0。代入例子(#2)的參數及上一步的結果,應執行10101000 & 11000011
結果爲10000000,清0成功。下面具體分析下$1是如何取出y最右邊n個二進制並與x中待修改的那n個二進制對齊的:
~(~0 << n)
和$0第一個步驟同樣,不過此次直接用這個結果看成掩碼。代入例子(#2)的參數,也就是~(~0 << 4)
,結果爲00001111。這裏將~(~0 << n)
記爲@@0
。y & @@0
,用掩碼的原理將y最右邊的n位取出。代入例子(#2)的參數及上一步的結果,應執行00000101 & 00001111
,結果爲00000101。這裏將y & @@0
記爲$$1 。$$1 << (p+1-n)
,左移到對應位置上。代入例子(#2)的參數及上一步的結果,也就是00000101 << (5+1-4)
,結果爲00010100,生成結束。 這裏會介紹一些runtime源碼中使用位運算的例子。
在runtime源碼中,判斷是不是TaggedPointer的函數定義以下:
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
複製代碼
其中參數const void * _Nullable ptr
爲對象的地址。_OBJC_TAG_MASK是一個掩碼
,其宏定義比較長,我將它簡單的整理了一下:
#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __x86_64__
// 64-bit Mac - tag bit is LSB
# define _OBJC_TAG_MASK 1UL
#else
// Everything else - tag bit is MSB
# define _OBJC_TAG_MASK (1UL<<63)
#endif
複製代碼
咱們獲得告終論:
根據以上結論,結合_objc_isTaggedPointer
函數的代碼,很容易理解它的原理:
在ARM64架構以前,對象的isa指針直接指向類對象地址;在ARM64架構以後,一個8字節的isa變量額外存儲了許多與當前對象相關的信息。 咱們先看來看一下最新的isa結構定義:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
複製代碼
相關的宏定義:
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \ uintptr_t nonpointer : 1; \ uintptr_t has_assoc : 1; \ uintptr_t has_cxx_dtor : 1; \ uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \ uintptr_t magic : 6; \ uintptr_t weakly_referenced : 1; \ uintptr_t deallocating : 1; \ uintptr_t has_sidetable_rc : 1; \ uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_BITFIELD \ uintptr_t nonpointer : 1; \ uintptr_t has_assoc : 1; \ uintptr_t has_cxx_dtor : 1; \ uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \ uintptr_t magic : 6; \ uintptr_t weakly_referenced : 1; \ uintptr_t deallocating : 1; \ uintptr_t has_sidetable_rc : 1; \ uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# error unknown architecture for packed isa
# endif
複製代碼
上面代碼中用了c語言的聯合體以及位段的技術,固然這不是咱們的重點,有興趣的話能夠去了解下。 我在Mac上編寫了一段代碼,用來展現這8個字節裏所存儲的數據有多麼豐富。要知道,8個字節僅僅是一個long變量的大小。
#import <Foundation/Foundation.h>
# define ISA_MASK 0x00007ffffffffff8ULL
union isa_t {
Class cls;
uintptr_t bits;
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8;
};
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc]init]; // 這塊內存記爲#1,obj指向#1,#1的引用計數器+1
NSObject *obj2 = obj; // obj2也指向#1,#1的引用計數器+1
NSObject *obj3 = obj; // obj3也指向#1,#1的引用計數器+1
__weak NSObject *weak_obj = obj;// 弱引用
union isa_t _isa_t;
void *_obj = (__bridge void *)(obj);
_isa_t.bits = *((uintptr_t*)_obj);
printf("是否使用isa指針優化:%x\n",_isa_t.nonpointer);
printf("是否有用關聯對象:%x\n",_isa_t.has_assoc);
printf("是否有C++析構函數:%x\n",_isa_t.has_cxx_dtor);
printf("isa取出類對象:%llx\n",_isa_t.bits & ISA_MASK);
printf("class方法取出類對象:%lx\n",(long)[NSObject class]);
printf("調試時是否完成初始化:%x\n",_isa_t.magic);
printf("是否有被弱引用指向過:%x\n",_isa_t.weakly_referenced);
printf("是否正在釋放:%x\n",_isa_t.deallocating);
printf("是否使用了sidetable:%x\n",_isa_t.has_sidetable_rc);
printf("引用計數器-1:%x\n",_isa_t.extra_rc);
}
return 0;
}
複製代碼
輸出的結果:
先給出Runtime源碼裏從緩存中查找方法的函數:
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);
bucket_t *b = buckets();
mask_t m = mask();
mask_t begin = cache_hash(k, m);
mask_t i = begin;
do {
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);
// hack
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}
複製代碼
再來看下cache_hash的實現:
// Class points to cache. SEL is key. Cache buckets store SEL+IMP.
// Caches are never built in the dyld shared cache.
static inline mask_t cache_hash(cache_key_t key, mask_t mask) {
return (mask_t)(key & mask);
}
複製代碼
這裏須要說明cache_hash函數中幾個參數的意義:
(key & mask)的結果能保證在[0,mask]整數範圍內,因此能夠正確的映射到Hash表上。
NS_OPTIONS如其名「選項」,可讓你在一個8字節NSUInteger變量中最多保存64個選項開關。 先來看看KVO中NSKeyValueObservingOptions的定義:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
NSKeyValueObservingOptionInitial = 0x04,
NSKeyValueObservingOptionPrior = 0x08
};
複製代碼
一共有4個選項,其對應的二進制分別爲:
能夠看得出,每一個選項都是獨立一個1,而且和其餘選項的位置不同。若是對某幾個選項進行或運算(|)就會合並它們的選項。 舉個平時經常使用的例子:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld 的結果爲:
下面給出讀取這些選項的代碼:
- (void)readOptions:(NSKeyValueObservingOptions)options {
NSLog(@"---------------begin---------------");
if (options & NSKeyValueObservingOptionNew ) {
NSLog(@"contain NSKeyValueObservingOptionNew");
}
if (options & NSKeyValueObservingOptionOld ) {
NSLog(@"contain NSKeyValueObservingOptionOld");
}
if (options & NSKeyValueObservingOptionInitial ) {
NSLog(@"contain NSKeyValueObservingOptionInitial");
}
if (options & NSKeyValueObservingOptionPrior ) {
NSLog(@"contain NSKeyValueObservingOptionPrior");
}
NSLog(@"---------------end-----------------");
}
// 輸出
/* ---------------begin--------------- contain NSKeyValueObservingOptionNew contain NSKeyValueObservingOptionOld ---------------end----------------- ---------------begin--------------- contain NSKeyValueObservingOptionNew contain NSKeyValueObservingOptionInitial contain NSKeyValueObservingOptionPrior ---------------end----------------- */
複製代碼
調用:
[self readOptions:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew];
[self readOptions:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial |NSKeyValueObservingOptionPrior];
複製代碼
輸出:
/* ---------------begin--------------- contain NSKeyValueObservingOptionNew contain NSKeyValueObservingOptionOld ---------------end----------------- ---------------begin--------------- contain NSKeyValueObservingOptionNew contain NSKeyValueObservingOptionInitial contain NSKeyValueObservingOptionPrior ---------------end----------------- */
複製代碼
本篇已同步到我的博客:位運算世界暢遊指南