位运算世界畅游指南
目錄
- 概念介紹
- 基本位運算符
- 按位與 ( AND )
- 按位或 ( OR )
- 按位異或 ( XOR )
- 取反 ( NOT )
- 右移 ( >> )
- 左移 ( << )
- 基礎技巧
- 將某些二進制位設置為1
- 掩碼
- 取出第i位二進制值
- 計算無符號變量的取值范圍
- 奇偶判斷
- 統計二進制中1個數
- 交換兩個變量的值(無臨時變量)
- 子集生成
- 進階技巧
- getBits
- setBits
- Objective-C的Runtime中的位運算應用
- 判斷是否是TaggedPointer
- isa_t
- 方法緩存中的Hash函數
- NS_OPTIONS
- 參考資料
概念介紹
我們都知道,計算機中的所有數據最終都是以二進制(bit)的形式存儲在計算機的。而在我們平時開發中所接觸數據的大多是字節為單位的,有了位運算之后我們就可以操作字節中的比特位了。在iOS的runtime源碼以及NS_OPTIONS 中都運用了位運算的知識,可見其重要性了。另外值得一提的是,大部分語言的位運算符都相同,所以這是一篇老少皆宜的文章。
閱讀本篇文章前,你需要知道的一些東西:
基本位運算符
位運算符就像是控制比特位的扳手,在學習位運算前先介紹下每個運算符的意義及其用法。
按位與 ( AND )
運算規則:只有在兩個值都為1時結果才為1,兩個值中有一個為0結果便為0。在編程語言里一般用 & 表示與運算符。
舉個例子,10100001 & 10001001 = 10000001。(注:操作數都為二進制。)
按位或 ( OR )
運算規則: 兩個值中有一個為1結果便為1,兩個值都為0時結果才為0。在編程語言里一般用 | 表示或運算符。
舉個例子,10100001 | 10001001 = 10101001。
按位異或 ( XOR )
運算規則: 只有當兩個值不相同時結果才為1,兩個值相同時結果為0。在編程語言里一般用 ^ 表示異或運算符。
舉個例子,10100001 ^ 10001001 = 00101000。
取反 ( NOT )
在數值的二進制表示方式上,將0變為1,將1變為0。在編程語言里一般用 ~ 表示取反運算符。
來看一個例子可能會更加直觀:
右移 ( >> )
右移將操作數的二進制位整體向右移動N位,空出部分用0填充,在編程語言里一般用 >>表示右移運算符。
舉個例子,下圖對二進制 10111101 右移3位后,最右邊的101被移除,左邊空出來3位用0填充(本文章默認所有數據都為無符號類型,所以本操作為邏輯右移)。
左移 ( << )
左移將操作數的二進制位整體向左移動N位,空出部分用0填充,在編程語言里一般用 << 表示左移運算符。
舉個例子,下圖對二進制 10111101 左移4位后,最左邊的1011被移除,右邊空出來4位用0填充。
基礎技巧
這里先介紹一些比較簡單實用的位運算技巧,這些技巧在日常開發中也是比較常用的。
將某些二進制位設置為1
假設x=0b10011010,現在我想將第5、6位置為1,將其變為0b11111010,那么執行 (x = x | 0b01100000) 就是我們想要的結果;那若是想將第0、5、6為置為1,變成0b11111011呢?那么執行(x = x | 0b01100001)就是我們想要的結果。 根據上面的兩個例子,我們可以得到一個結論:
- x = x | SET_ON ,該語句將x中對應于SET_ON中為1的二進制位設置為1;x中對應于SET_ON中為0的二進制位保持不變。
掩碼
掩碼這個詞經常能在計算機網絡里聽到,比較熟悉的話就是子網掩碼了。掩碼是起的非常好的一個名字,當我們的操作數和掩碼進行與運算(&)后,掩碼中二進制為0的位會屏蔽掉原操作數對應的二進制位。 舉個例子,假如現在我有一個2個字節的數據0xBA15,若要屏蔽掉中間0xA1這8位二進制變成0xB005,該如何設計掩碼呢?答案很簡單,只要將掩碼中間8位設為0其他設為1即可,所以本例中的掩碼應為0xF00F,0xBA15 & 0xF00F=0xB005。可以結合下圖理解:
取出第i位二進制值
這個函數傳入一個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的變量xprintf("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),故為偶數,大家可以多寫幾組數字驗證。
關于負數:雖然負數在計算機中以補碼的方式存儲,但由于補碼最后一位和原碼最后一位相同,所以上面的函數同樣適用于負數。為什么呢?舉個例子:
- 假如最后一位是0,取反后變成1,然后再+1又變成0。
- 假如最后一位是1,取反后變成0,然后再+1又變成1。
看到了吧,最后又變回去了。
統計二進制中1個數
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呢?舉個例子:
- 假如x=101000,那么 x-1=100111。
- 假如x=101011,那么 x-1=101010。
可以發現規律:
- 當x=nn..nn100..00這種形式時,x-1=nn..nn011..11。 這個時候x & (x-1) = nn..nn100..00 & nn..nn011..11 = nn..nn000.00,x最后一個1被消去。
- 當x=nn..nn1這種形式時,x-1=nn..nn0。 這個時候x & (x-1) = nn..nn1 & nn..nn0 = nn..nn0,x最后一個1被消去。
交換兩個變量的值(無臨時變量)
// 注:參數是c++的引用類型。 void swap(int &a,int &b) {a=a^b;b=a^b;a=a^b; } 復制代碼想要了解原理,需要先知道幾個異或運算的性質:
- 交換律:a^b = b^a
- 結合律:(a^b)^c = a^(b^c)
- 恒等律:a^0 = a
- 規零律:a^a = 0
- 自反性:a^b^b = a
假設一開始,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-bafter 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的所有子集:
| 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語言實現的代碼:
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語言程序設計這本書中的兩個非常實用的函數,相信你在平時的項目中也能應用的到。
getBits
該函數用來返回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) ,我們一起來分析下過程。
- 一開始執行~0生成全1的二進制,11111111。
- 然后左移3位變成11111000。
- 最后執行圓括號左邊的~,取反變成00000111,現在掩碼生成完成。
- 最后執行中間的那個&,將(x>>(p+1-n))和~(~0<<n)與運算,取出期望字段。對于上面的例子,對應過程圖如下:
setBits
該函數返回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是為了將x中期望修改的n個二進制清0。在例子(#2)中,$0返回的二進制應該為:10000000,注意到紅體部分已經被清0。
- $1是為了取出y最右邊n個二進制,并與x中待修改的那n個二進制對齊。在例子(#2)中,$1返回的二進制應該:00010100。
- 最后$0 | $1 ,也就是將$1的值設置到$0當中。在在例子(#2)中,$0 | $1 = 10000000 | 00010100 = 10010100,設置完成。
下面具體分析下$0是如何將期望修改的n個二進制清0的:
- 既然是清0,我們可以想使用到最早所學的掩碼,所以可以將$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,生成結束。
Objective-C的Runtime中的位運算應用
這里會介紹一些runtime源碼中使用位運算的例子。
判斷是否是TaggedPointer
在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是一個掩碼,其宏定義比較長,我將它簡單的整理了一下:
// 64-bit Mac - tag bit is LSB // Everything else - tag bit is MSB 復制代碼我們得到了結論:
- 64-bit Mac下, _OBJC_TAG_MASK為1UL,對應二進制為:00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
- 其他平臺下, _OBJC_TAG_MASK 為(1UL<<63),對應二進制為:10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
根據以上結論,結合_objc_isTaggedPointer函數的代碼,很容易理解它的原理:
- 在64-bit MAC下,取出對象地址二進制的最低位(LSB),若最低位為1則為TaggedPointer。
- 在其他平臺下,取出對象地址二進制的最高位(MSB),若為1則為TaggedPointer。
isa_t
在ARM64架構之前,對象的isa指針直接指向類對象地址;在ARM64架構之后,一個8字節的isa變量額外存儲了許多與當前對象相關的信息。 我們先看來看一下最新的isa結構定義:
union isa_t {isa_t() { }isa_t(uintptr_t value) : bits(value) { }Class cls;uintptr_t bits; struct {ISA_BITFIELD; // defined in isa.h}; };復制代碼相關的宏定義:
復制代碼上面代碼中用了c語言的聯合體以及位段的技術,當然這不是我們的重點,有興趣的話可以去了解下。 我在Mac上編寫了一段代碼,用來展示這8個字節里所存儲的數據有多么豐富。要知道,8個字節僅僅是一個long變量的大小。
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的引用計數器+1NSObject *obj2 = obj; // obj2也指向#1,#1的引用計數器+1NSObject *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; } 復制代碼輸出的結果:
- 是否使用isa指針優化:1
- 是否有用關聯對象:0
- 是否有C++析構函數:0
- isa取出類對象:7fffb506f140
- class方法取出類對象:7fffb506f140
- 調試時是否完成初始化:3b
- 是否有被弱引用指向過:1
- 是否正在釋放:0
- 是否使用了sidetable:0
- 引用計數器-1:2
方法緩存中的Hash函數
先給出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);// hackClass 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: 方法SEL的地址(8字節64位)
- mask: 哈希表長度 -1
(key & mask)的結果能保證在[0,mask]整數范圍內,所以可以正確的映射到Hash表上。
NS_OPTIONS
NS_OPTIONS如其名「選項」,可以讓你在一個8字節NSUInteger變量中最多保存64個選項開關。 先來看看KVO中NSKeyValueObservingOptions的定義:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {NSKeyValueObservingOptionNew = 0x01,NSKeyValueObservingOptionOld = 0x02,NSKeyValueObservingOptionInitial = 0x04,NSKeyValueObservingOptionPrior = 0x08 }; 復制代碼一共有4個選項,其對應的二進制分別為:
- 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000001
- 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000010
- 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000100
- 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000001000
可以看得出,每個選項都是獨立一個1,并且和其他選項的位置不一樣。如果對某幾個選項進行或運算(|)就會合并它們的選項。 舉個平時常用的例子:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld 的結果為:
- 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 000000011
下面給出讀取這些選項的代碼:
- (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 NSKeyValueObservingOptionNewcontain NSKeyValueObservingOptionOld---------------end--------------------------------begin---------------contain NSKeyValueObservingOptionNewcontain NSKeyValueObservingOptionInitialcontain NSKeyValueObservingOptionPrior---------------end----------------- */ 復制代碼調用:
[self readOptions:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew];[self readOptions:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial |NSKeyValueObservingOptionPrior]; 復制代碼輸出:
/*---------------begin---------------contain NSKeyValueObservingOptionNewcontain NSKeyValueObservingOptionOld---------------end--------------------------------begin---------------contain NSKeyValueObservingOptionNewcontain NSKeyValueObservingOptionInitialcontain NSKeyValueObservingOptionPrior---------------end----------------- */ 復制代碼參考資料
- 《C語言程序設計(K & R)》
本篇已同步到個人博客:位運算世界暢游指南
轉載于:https://juejin.im/post/5ce1324af265da1b8f1a9162
總結
- 上一篇: kubernetes一次生产故障日记
- 下一篇: PDF页眉页脚怎么设置