日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

位运算世界畅游指南

發(fā)布時(shí)間:2025/4/5 编程问答 51 豆豆
生活随笔 收集整理的這篇文章主要介紹了 位运算世界畅游指南 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

目錄

  • 概念介紹
  • 基本位運(yùn)算符
    • 按位與 ( AND )
    • 按位或 ( OR )
    • 按位異或 ( XOR )
    • 取反 ( NOT )
    • 右移 ( >> )
    • 左移 ( << )
  • 基礎(chǔ)技巧
    • 將某些二進(jìn)制位設(shè)置為1
    • 掩碼
    • 取出第i位二進(jìn)制值
    • 計(jì)算無(wú)符號(hào)變量的取值范圍
    • 奇偶判斷
    • 統(tǒng)計(jì)二進(jìn)制中1個(gè)數(shù)
    • 交換兩個(gè)變量的值(無(wú)臨時(shí)變量)
    • 子集生成
  • 進(jìn)階技巧
    • getBits
    • setBits
  • Objective-C的Runtime中的位運(yùn)算應(yīng)用
    • 判斷是否是TaggedPointer
    • isa_t
    • 方法緩存中的Hash函數(shù)
  • NS_OPTIONS
  • 參考資料

概念介紹

我們都知道,計(jì)算機(jī)中的所有數(shù)據(jù)最終都是以二進(jìn)制(bit)的形式存儲(chǔ)在計(jì)算機(jī)的。而在我們平時(shí)開發(fā)中所接觸數(shù)據(jù)的大多是字節(jié)為單位的,有了位運(yùn)算之后我們就可以操作字節(jié)中的比特位了。在iOS的runtime源碼以及NS_OPTIONS 中都運(yùn)用了位運(yùn)算的知識(shí),可見其重要性了。另外值得一提的是,大部分語(yǔ)言的位運(yùn)算符都相同,所以這是一篇老少皆宜的文章。

閱讀本篇文章前,你需要知道的一些東西:

  • 我們?cè)谟懻摱M(jìn)制的時(shí)候,位序一般都是從右(低位)到左(高位)的。舉個(gè)例子,對(duì)于二進(jìn)制0011來(lái)說(shuō),第0位是1,第1位是1,第二位是0,第三位是0。
  • 在大部分編程語(yǔ)言中,0x開頭代表十六進(jìn)制,比如0xff代表十六進(jìn)制ff;0b開頭代表二進(jìn)制,比如0b11111111代表二進(jìn)制的11111111;0開頭代表8進(jìn)制,比如0377代表8進(jìn)制377;如果你不寫前綴,那默認(rèn)就是10進(jìn)制。無(wú)論你是幾進(jìn)制,你所描述的本質(zhì)其實(shí)是一樣的,只是表現(xiàn)形式不同而已。比如前面的0xff、0b11111111、0377、255,它們都是等價(jià)的。你可以在編程語(yǔ)言中語(yǔ)言測(cè)試下。
  • bool c = 0xff == 0b11111111; // truebool c0 = 0b11111111 == 0377; // truebool c1 = 0377 == 255; //true 復(fù)制代碼
  • 之所以程序員都喜歡用16進(jìn)制,首先是因?yàn)?6進(jìn)制和二進(jìn)制的轉(zhuǎn)換實(shí)在太方便了。比如0xFAFA,直把每個(gè)字母轉(zhuǎn)成4位二進(jìn)制拼接在一起即可,0xF:0b1111 ,0xA:0b1010,所以0xFAFA二進(jìn)制是0b1111101011111010。另外一點(diǎn),因?yàn)?6進(jìn)制中2個(gè)字母代表8位二進(jìn)制(一個(gè)字節(jié)),所以當(dāng)我們看到16進(jìn)制的時(shí)候就能立馬知道多少個(gè)字節(jié)了。比如前面的0xFAFA,第一個(gè)字節(jié)是0xFA,第二個(gè)字節(jié)也是0xFA,共兩個(gè)字節(jié)。
  • 基本位運(yùn)算符

    位運(yùn)算符就像是控制比特位的扳手,在學(xué)習(xí)位運(yùn)算前先介紹下每個(gè)運(yùn)算符的意義及其用法。

    按位與 ( AND )

    運(yùn)算規(guī)則:只有在兩個(gè)值都為1時(shí)結(jié)果才為1,兩個(gè)值中有一個(gè)為0結(jié)果便為0。在編程語(yǔ)言里一般用 & 表示與運(yùn)算符。


    舉個(gè)例子,10100001 & 10001001 = 10000001。(注:操作數(shù)都為二進(jìn)制。)

    按位或 ( OR )

    運(yùn)算規(guī)則: 兩個(gè)值中有一個(gè)為1結(jié)果便為1,兩個(gè)值都為0時(shí)結(jié)果才為0。在編程語(yǔ)言里一般用 | 表示或運(yùn)算符。


    舉個(gè)例子,10100001 | 10001001 = 10101001。


    按位異或 ( XOR )

    運(yùn)算規(guī)則: 只有當(dāng)兩個(gè)值不相同時(shí)結(jié)果才為1,兩個(gè)值相同時(shí)結(jié)果為0。在編程語(yǔ)言里一般用 ^ 表示異或運(yùn)算符。


    舉個(gè)例子,10100001 ^ 10001001 = 00101000。

    取反 ( NOT )

    在數(shù)值的二進(jìn)制表示方式上,將0變?yōu)?,將1變?yōu)?。在編程語(yǔ)言里一般用 ~ 表示取反運(yùn)算符。
    來(lái)看一個(gè)例子可能會(huì)更加直觀:

    右移 ( >> )

    右移將操作數(shù)的二進(jìn)制位整體向右移動(dòng)N位,空出部分用0填充,在編程語(yǔ)言里一般用 >>表示右移運(yùn)算符。

    舉個(gè)例子,下圖對(duì)二進(jìn)制 10111101 右移3位后,最右邊的101被移除,左邊空出來(lái)3位用0填充(本文章默認(rèn)所有數(shù)據(jù)都為無(wú)符號(hào)類型,所以本操作為邏輯右移)。


    左移 ( << )

    左移將操作數(shù)的二進(jìn)制位整體向左移動(dòng)N位,空出部分用0填充,在編程語(yǔ)言里一般用 << 表示左移運(yùn)算符。

    舉個(gè)例子,下圖對(duì)二進(jìn)制 10111101 左移4位后,最左邊的1011被移除,右邊空出來(lái)4位用0填充。

    基礎(chǔ)技巧

    這里先介紹一些比較簡(jiǎn)單實(shí)用的位運(yùn)算技巧,這些技巧在日常開發(fā)中也是比較常用的。

    將某些二進(jìn)制位設(shè)置為1

    假設(shè)x=0b10011010,現(xiàn)在我想將第5、6位置為1,將其變?yōu)?b11111010,那么執(zhí)行 (x = x | 0b01100000) 就是我們想要的結(jié)果;那若是想將第0、5、6為置為1,變成0b11111011呢?那么執(zhí)行(x = x | 0b01100001)就是我們想要的結(jié)果。 根據(jù)上面的兩個(gè)例子,我們可以得到一個(gè)結(jié)論:

    • x = x | SET_ON ,該語(yǔ)句將x中對(duì)應(yīng)于SET_ON中為1的二進(jìn)制位設(shè)置為1;x中對(duì)應(yīng)于SET_ON中為0的二進(jìn)制位保持不變。

    掩碼

    掩碼這個(gè)詞經(jīng)常能在計(jì)算機(jī)網(wǎng)絡(luò)里聽到,比較熟悉的話就是子網(wǎng)掩碼了。掩碼是起的非常好的一個(gè)名字,當(dāng)我們的操作數(shù)和掩碼進(jìn)行與運(yùn)算(&)后,掩碼中二進(jìn)制為0的位會(huì)屏蔽掉原操作數(shù)對(duì)應(yīng)的二進(jìn)制位。 舉個(gè)例子,假如現(xiàn)在我有一個(gè)2個(gè)字節(jié)的數(shù)據(jù)0xBA15,若要屏蔽掉中間0xA1這8位二進(jìn)制變成0xB005,該如何設(shè)計(jì)掩碼呢?答案很簡(jiǎn)單,只要將掩碼中間8位設(shè)為0其他設(shè)為1即可,所以本例中的掩碼應(yīng)為0xF00F,0xBA15 & 0xF00F=0xB005。可以結(jié)合下圖理解:


    取出第i位二進(jìn)制值

    這個(gè)函數(shù)傳入一個(gè)data,返回其二進(jìn)制從右邊開始數(shù)第i位的值。

    unsigned char getBit( unsigned long data , int i ) {// i = 0時(shí),代表取最右邊的哪一位。data = data >> i ;return data & 1 ; } 復(fù)制代碼

    原理很簡(jiǎn)單,先將data右移i位,這樣能保證第i位的值處于data的最右邊,然后再用data & 1取出即可。 舉個(gè)例子,如果我調(diào)用了{(lán)getBit(168,3)},168對(duì)應(yīng)的二進(jìn)制為10101000,右移3位后變成00010101,最后00010101 & 00000001 = 1,取出成功。

    計(jì)算無(wú)符號(hào)變量的取值范圍

    筆者在mac上的unsigned long 是8個(gè)字節(jié),可以存儲(chǔ)64位二進(jìn)制,由于沒有符號(hào)位,故只需將這64位二進(jìn)制都填充為1就得到unsigned long變量的最大值了。

    // 將全0取反變?yōu)槿?裝進(jìn)變量x中。unsigned long x = ~0;// 輸出二進(jìn)制為全1的變量xprintf("unsigned long max = %lu\n",x);復(fù)制代碼

    奇偶判斷

    如果最后一位二進(jìn)制為0,那么這個(gè)數(shù)字就是偶數(shù),如果為1就是奇數(shù)。這里給出實(shí)現(xiàn)函數(shù):

    int isOdd(int value) {return (value & 1); // 取出最后一位二進(jìn)制,若為1則是奇數(shù) } 復(fù)制代碼

    說(shuō)下大致原理:若最后一位二進(jìn)制為0,那么二進(jìn)制轉(zhuǎn)成十進(jìn)制后必然可以寫成2n的形式,必為偶數(shù)。比如我隨便寫一個(gè)最后一位為0的二進(jìn)制數(shù)字 10001010,那么其十進(jìn)制數(shù)為2+2^3+2^7 = 2*(1+2^2+2^6),故為偶數(shù),大家可以多寫幾組數(shù)字驗(yàn)證。

    關(guān)于負(fù)數(shù):雖然負(fù)數(shù)在計(jì)算機(jī)中以補(bǔ)碼的方式存儲(chǔ),但由于補(bǔ)碼最后一位和原碼最后一位相同,所以上面的函數(shù)同樣適用于負(fù)數(shù)。為什么呢?舉個(gè)例子:

    • 假如最后一位是0,取反后變成1,然后再+1又變成0。
    • 假如最后一位是1,取反后變成0,然后再+1又變成1。

    看到了吧,最后又變回去了。

    統(tǒng)計(jì)二進(jìn)制中1個(gè)數(shù)

    int x =0xba;//10111010 int count = 0; while (x!=0) {x = x&(x-1);count++; } printf("%d\n",count); 復(fù)制代碼

    循環(huán)中每次執(zhí)行x = x&(x-1)后,x的二進(jìn)制的最后一個(gè)1就會(huì)被消去。當(dāng)所有1都被消去后,count計(jì)數(shù)完畢,x=0,退出循環(huán)。

    那么為什么x = x&(x-1)能夠消去其二進(jìn)制的最后一個(gè)1呢?舉個(gè)例子:

    • 假如x=101000,那么 x-1=100111。
    • 假如x=101011,那么 x-1=101010。

    可以發(fā)現(xiàn)規(guī)律:

    • 當(dāng)x=nn..nn100..00這種形式時(shí),x-1=nn..nn011..11。 這個(gè)時(shí)候x & (x-1) = nn..nn100..00 & nn..nn011..11 = nn..nn000.00,x最后一個(gè)1被消去。
    • 當(dāng)x=nn..nn1這種形式時(shí),x-1=nn..nn0。 這個(gè)時(shí)候x & (x-1) = nn..nn1 & nn..nn0 = nn..nn0,x最后一個(gè)1被消去。

    交換兩個(gè)變量的值(無(wú)臨時(shí)變量)

    // 注:參數(shù)是c++的引用類型。 void swap(int &a,int &b) {a=a^b;b=a^b;a=a^b; } 復(fù)制代碼

    想要了解原理,需要先知道幾個(gè)異或運(yùn)算的性質(zhì):

    • 交換律:a^b = b^a
    • 結(jié)合律:(a^b)^c = a^(b^c)
    • 恒等律:a^0 = a
    • 規(guī)零律:a^a = 0
    • 自反性:a^b^b = a

    假設(shè)一開始,a=k,b=t。

    • 執(zhí)行a=a^b后 a=k^t;
    • 執(zhí)行b=a^b后 b = k^t^t=k,注意這里用到了自反性;
    • 執(zhí)行a=a^b后 a=k^t^k=t^k^k=t,注意這里用到了交換律和自反性;
    • 最后得到a=t,b=k,交換完成。

    當(dāng)然了,不僅限于交換整型變量。舉一個(gè)不太常用的例子,我們可以不用臨時(shí)變量交換兩個(gè)c語(yǔ)言字符串。下面代碼中的a和b本質(zhì)上是在交換"a-a-a-a-a-a"和"b-b-b-b-b-b"地址,所以效果也是一樣的。

    char *a = "a-a-a-a-a-a";// 存儲(chǔ)在數(shù)據(jù)區(qū)的字符串常量char *b = "b-b-b-b-b-b";//存儲(chǔ)在數(shù)據(jù)區(qū)的字符串常量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*/ 復(fù)制代碼

    子集生成

    假設(shè)現(xiàn)在有一集合A={a,b,c},要求生成這個(gè)集合的所有子集。構(gòu)造子集時(shí),我們可以使用二進(jìn)制中第i位的值決定是否要選取集合A中的第i個(gè)元素。其中值為1代表選取,值為0代表不選取。舉個(gè)例子,100代表只選第一個(gè)元素a,其構(gòu)成的子集為{a};101代表選取第一個(gè)a以及第三個(gè)c,其構(gòu)成的子集為{a,c}。

    下面列舉出A的所有子集:

    編號(hào)A的子集人類思考過(guò)程二進(jìn)制表示
    0{}什么都不選000
    1{c}不選a,不選b,選c001
    2{b}不選a,選b,不選c010
    3{b,c}不選a,選b,選c011
    4{a}選a,不選b,不選c100
    5{a,c}選a,不選b,選c101
    6{a,b}選a,選b,不選c110
    7{a,b,c}選a,選b,選c111

    細(xì)心的話,應(yīng)該能發(fā)現(xiàn)上面的表格中的編號(hào)和二進(jìn)制剛剛好能對(duì)的上。所以對(duì)于有個(gè)n個(gè)元素的集合,只要生成0到2^n-1個(gè)整數(shù)編號(hào),然后根據(jù)每個(gè)編號(hào)對(duì)應(yīng)的二進(jìn)制解析出相應(yīng)的子集即可。 下面是c語(yǔ)言實(shí)現(xiàn)的代碼:

    #include <stdio.h> #include <string.h> void prinstSubSet(char *S,int id) {int n = (int)strlen(S);// 集合的元素個(gè)數(shù)char result[100];int index=0;printf("{");for(int i=n-1;i>=0;i--){if ((id>>i)&1) {// 若第i位值為1,代表選擇第i個(gè)元素(從右邊開始數(shù))result[index++]=S[n-1-i];//由于字符串第0個(gè)字符是最左邊,所以要顛倒下。}}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);// 集合的元素個(gè)數(shù)int begin = 0;int end = (1<<n)-1; // 2^n-1//生成0到2^n-1個(gè)編號(hào)(id)for (int id = begin;id<=end;id++) {prinstSubSet(S, id);// 根據(jù)編號(hào)對(duì)應(yīng)的二進(jìn)制輸出子集} } int main(int argc, const char * argv[]) {create("abc");// 生成{a,b,c}的子集return 0; } 復(fù)制代碼

    進(jìn)階技巧

    這里介紹C語(yǔ)言程序設(shè)計(jì)這本書中的兩個(gè)非常實(shí)用的函數(shù),相信你在平時(shí)的項(xiàng)目中也能應(yīng)用的到。

    getBits

    該函數(shù)用來(lái)返回x中從右邊數(shù)第p位開始向右數(shù)n位二進(jìn)制。

    unsigned getBits(unsigned x,int p,int n) {return (x>>(p+1-n)) & ~(~0<<n); } 復(fù)制代碼

    舉個(gè)例子,調(diào)用getBits(168,5,3)后,返回168對(duì)應(yīng)二進(jìn)制10101000從右邊數(shù)第5位開始向右3位二進(jìn)制,也就是101。可以結(jié)合下圖理解:

    下面來(lái)說(shuō)以下原理:
    • 一開始執(zhí)行(x>>(p+1-n)) 這里是為了將期望獲得的字段移動(dòng)到最右邊。用上面的例子,執(zhí)行完后x變成:


    • ~(~0<<n) 是為了生成右邊n位全1的掩碼。 對(duì)于上面的例子~(~0<<3) ,我們一起來(lái)分析下過(guò)程。

    • 一開始執(zhí)行~0生成全1的二進(jìn)制,11111111。
    • 然后左移3位變成11111000。
    • 最后執(zhí)行圓括號(hào)左邊的~,取反變成00000111,現(xiàn)在掩碼生成完成。

    • 最后執(zhí)行中間的那個(gè)&,將(x>>(p+1-n))和~(~0<<n)與運(yùn)算,取出期望字段。對(duì)于上面的例子,對(duì)應(yīng)過(guò)程圖如下:

    setBits

    該函數(shù)返回x中從第p位開始的n個(gè)(二進(jìn)制)位設(shè)置為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); } 復(fù)制代碼

    舉個(gè)例子(#2),調(diào)用setbits(168, 5, 4, 0b0101)后,將168對(duì)應(yīng)二進(jìn)制10101000從右邊數(shù)第5位開始的4個(gè)二進(jìn)制位設(shè)置為0101,設(shè)置完后變成10010100,最后將結(jié)果返回。可以結(jié)合下圖理解:

    第一眼看到這個(gè)函數(shù)代碼還是有一些恐怖的,不用害怕,我們一層層解析,相信你一定能感受位運(yùn)算的精妙之處!

    我們先要將函數(shù)拆成兩個(gè)部分看待,第一部分是( x & ~( ~( ~0 << n ) << ( p+1-n ) ) )記為$0;另一部分是(y & ~(~0 << n) ) << (p+1-n)記為$1。 下面分析下$0和$1的作用:

    • 其中,$0是為了將x中期望修改的n個(gè)二進(jìn)制清0。在例子(#2)中,$0返回的二進(jìn)制應(yīng)該為:10000000,注意到紅體部分已經(jīng)被清0。
    • $1是為了取出y最右邊n個(gè)二進(jìn)制,并與x中待修改的那n個(gè)二進(jìn)制對(duì)齊。在例子(#2)中,$1返回的二進(jìn)制應(yīng)該:00010100。
    • 最后$0 | $1 ,也就是將$1的值設(shè)置到$0當(dāng)中。在在例子(#2)中,$0 | $1 = 10000000 | 00010100 = 10010100,設(shè)置完成。

    下面具體分析下$0是如何將期望修改的n個(gè)二進(jìn)制清0的:

    • 既然是清0,我們可以想使用到最早所學(xué)的掩碼,所以可以將$0以&為分割符拆成兩段看待,其中~( ~( ~0 << n ) << ( p+1-n ) )生成x清0所需要的掩碼。
    • 一開始執(zhí)行 ~(~0 << n) 生成右邊n個(gè)1,其余全為0的。代入例子(#2)的參數(shù),也就是~(~0 << 4),結(jié)果為:00001111。這里為了方便記憶,把~(~0 << n)記為$$0 。
    • 然后接著執(zhí)行$$0 << (p+1-n),將右邊n個(gè)1左移到相應(yīng)位置上。代入例子(#2)的參數(shù)及上一步的結(jié)果,應(yīng)執(zhí)行00001111 << (5+1-4),結(jié)果為00111100。這里將$$0 << (p+1-n)記為$$1。
    • 最后執(zhí)行最外層~$$1,生成清零所需的掩碼。代入例子(#2)的參數(shù)及上一步的結(jié)果,應(yīng)執(zhí)行~00111100 ,結(jié)果為11000011,掩碼生成完畢。
    • 最后執(zhí)行 x & ~$$1,用掩碼將x中待清零的位清0。代入例子(#2)的參數(shù)及上一步的結(jié)果,應(yīng)執(zhí)行10101000 & 11000011結(jié)果為10000000,清0成功。

    下面具體分析下$1是如何取出y最右邊n個(gè)二進(jìn)制并與x中待修改的那n個(gè)二進(jìn)制對(duì)齊的:

    • 首先 ~(~0 << n)和$0第一個(gè)步驟一樣,不過(guò)這次直接用這個(gè)結(jié)果當(dāng)作掩碼。代入例子(#2)的參數(shù),也就是~(~0 << 4),結(jié)果為00001111。這里將~(~0 << n)記為@@0。
    • 接著 執(zhí)行 y & @@0 ,用掩碼的原理將y最右邊的n位取出。代入例子(#2)的參數(shù)及上一步的結(jié)果,應(yīng)執(zhí)行00000101 & 00001111,結(jié)果為00000101。這里將y & @@0記為$$1 。
    • 最后執(zhí)行 $$1 << (p+1-n),左移到對(duì)應(yīng)位置上。代入例子(#2)的參數(shù)及上一步的結(jié)果,也就是00000101 << (5+1-4),結(jié)果為00010100,生成結(jié)束。

    Objective-C的Runtime中的位運(yùn)算應(yīng)用


    這里會(huì)介紹一些runtime源碼中使用位運(yùn)算的例子。

    判斷是否是TaggedPointer

    在runtime源碼中,判斷是否是TaggedPointer的函數(shù)定義如下:

    static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) {return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK; } 復(fù)制代碼

    其中參數(shù)const void * _Nullable ptr為對(duì)象的地址。_OBJC_TAG_MASK是一個(gè)掩碼,其宏定義比較長(zhǎng),我將它簡(jiǎn)單的整理了一下:

    #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 復(fù)制代碼

    我們得到了結(jié)論:

    • 64-bit Mac下, _OBJC_TAG_MASK為1UL,對(duì)應(yīng)二進(jìn)制為:00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
    • 其他平臺(tái)下, _OBJC_TAG_MASK 為(1UL<<63),對(duì)應(yīng)二進(jìn)制為:10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

    根據(jù)以上結(jié)論,結(jié)合_objc_isTaggedPointer函數(shù)的代碼,很容易理解它的原理:

    • 在64-bit MAC下,取出對(duì)象地址二進(jìn)制的最低位(LSB),若最低位為1則為TaggedPointer。
    • 在其他平臺(tái)下,取出對(duì)象地址二進(jìn)制的最高位(MSB),若為1則為TaggedPointer。

    isa_t

    在ARM64架構(gòu)之前,對(duì)象的isa指針直接指向類對(duì)象地址;在ARM64架構(gòu)之后,一個(gè)8字節(jié)的isa變量額外存儲(chǔ)了許多與當(dāng)前對(duì)象相關(guān)的信息。 我們先看來(lái)看一下最新的isa結(jié)構(gòu)定義:

    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 };復(fù)制代碼

    相關(guān)的宏定義:

    # 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 復(fù)制代碼

    上面代碼中用了c語(yǔ)言的聯(lián)合體以及位段的技術(shù),當(dāng)然這不是我們的重點(diǎn),有興趣的話可以去了解下。 我在Mac上編寫了一段代碼,用來(lái)展示這8個(gè)字節(jié)里所存儲(chǔ)的數(shù)據(jù)有多么豐富。要知道,8個(gè)字節(jié)僅僅是一個(gè)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]; // 這塊內(nèi)存記為#1,obj指向#1,#1的引用計(jì)數(shù)器+1NSObject *obj2 = obj; // obj2也指向#1,#1的引用計(jì)數(shù)器+1NSObject *obj3 = obj; // obj3也指向#1,#1的引用計(jì)數(shù)器+1__weak NSObject *weak_obj = obj;// 弱引用union isa_t _isa_t;void *_obj = (__bridge void *)(obj);_isa_t.bits = *((uintptr_t*)_obj);printf("是否使用isa指針優(yōu)化:%x\n",_isa_t.nonpointer);printf("是否有用關(guān)聯(lián)對(duì)象:%x\n",_isa_t.has_assoc);printf("是否有C++析構(gòu)函數(shù):%x\n",_isa_t.has_cxx_dtor);printf("isa取出類對(duì)象:%llx\n",_isa_t.bits & ISA_MASK);printf("class方法取出類對(duì)象:%lx\n",(long)[NSObject class]);printf("調(diào)試時(shí)是否完成初始化:%x\n",_isa_t.magic);printf("是否有被弱引用指向過(guò):%x\n",_isa_t.weakly_referenced);printf("是否正在釋放:%x\n",_isa_t.deallocating);printf("是否使用了sidetable:%x\n",_isa_t.has_sidetable_rc);printf("引用計(jì)數(shù)器-1:%x\n",_isa_t.extra_rc);}return 0; } 復(fù)制代碼

    輸出的結(jié)果:

    • 是否使用isa指針優(yōu)化:1
    • 是否有用關(guān)聯(lián)對(duì)象:0
    • 是否有C++析構(gòu)函數(shù):0
    • isa取出類對(duì)象:7fffb506f140
    • class方法取出類對(duì)象:7fffb506f140
    • 調(diào)試時(shí)是否完成初始化:3b
    • 是否有被弱引用指向過(guò):1
    • 是否正在釋放:0
    • 是否使用了sidetable:0
    • 引用計(jì)數(shù)器-1:2

    方法緩存中的Hash函數(shù)

    先給出Runtime源碼里從緩存中查找方法的函數(shù):

    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); } 復(fù)制代碼

    再來(lái)看下cache_hash的實(shí)現(xiàn):

    // 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); } 復(fù)制代碼

    這里需要說(shuō)明cache_hash函數(shù)中幾個(gè)參數(shù)的意義:

    • key: 方法SEL的地址(8字節(jié)64位)
    • mask: 哈希表長(zhǎng)度 -1

    (key & mask)的結(jié)果能保證在[0,mask]整數(shù)范圍內(nèi),所以可以正確的映射到Hash表上。

    NS_OPTIONS

    NS_OPTIONS如其名「選項(xiàng)」,可以讓你在一個(gè)8字節(jié)NSUInteger變量中最多保存64個(gè)選項(xiàng)開關(guān)。 先來(lái)看看KVO中NSKeyValueObservingOptions的定義:

    typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {NSKeyValueObservingOptionNew = 0x01,NSKeyValueObservingOptionOld = 0x02,NSKeyValueObservingOptionInitial = 0x04,NSKeyValueObservingOptionPrior = 0x08 }; 復(fù)制代碼

    一共有4個(gè)選項(xiàng),其對(duì)應(yīng)的二進(jìn)制分別為:

    • 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

    可以看得出,每個(gè)選項(xiàng)都是獨(dú)立一個(gè)1,并且和其他選項(xiàng)的位置不一樣。如果對(duì)某幾個(gè)選項(xiàng)進(jìn)行或運(yùn)算(|)就會(huì)合并它們的選項(xiàng)。 舉個(gè)平時(shí)常用的例子:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld 的結(jié)果為:

    • 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 000000011

    下面給出讀取這些選項(xiàng)的代碼:

    - (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----------------- */ 復(fù)制代碼

    調(diào)用:

    [self readOptions:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew];[self readOptions:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial |NSKeyValueObservingOptionPrior]; 復(fù)制代碼

    輸出:

    /*---------------begin---------------contain NSKeyValueObservingOptionNewcontain NSKeyValueObservingOptionOld---------------end--------------------------------begin---------------contain NSKeyValueObservingOptionNewcontain NSKeyValueObservingOptionInitialcontain NSKeyValueObservingOptionPrior---------------end----------------- */ 復(fù)制代碼

    參考資料

    • 《C語(yǔ)言程序設(shè)計(jì)(K & R)》

    本篇已同步到個(gè)人博客:位運(yùn)算世界暢游指南

    轉(zhuǎn)載于:https://juejin.im/post/5ce1324af265da1b8f1a9162

    總結(jié)

    以上是生活随笔為你收集整理的位运算世界畅游指南的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。