1. 程式人生 > >iOS底層原理之`OC語法`(Block)

iOS底層原理之`OC語法`(Block)

block本質

對於iOS中的block很對人都會說是封裝了一塊程式碼或者是說就是一個程式碼塊,這種回答雖然是對的,但是很淺顯。那block究竟是個什麼東西呢?我們可以編譯後看一下底層的實現。
原檔案:
示例:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        void (^blocktest)(void) = ^{
            NSLog(@"%d",a);
        };
        blocktest();
       
    }
    return 0;
}

編譯後的cpp檔案

//mian函式
int main(int argc, const char * argv[]) {
    { __AtAutoreleasePool __autoreleasepool; 
        int a = 10;
        //block定義
        void (*blocktest)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        //block呼叫
        ((void (*)(__block_impl *))((__block_impl *)blocktest)->FuncPtr)((__block_impl *)blocktest);

    }
    return 0;
}

//block定義
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock; //指向block的真實型別
    impl.Flags = flags;
    impl.FuncPtr = fp;//用於block呼叫
    Desc = desc;//block的一些描述
  }
};

//__main_block_impl_0結構體中的第一個元素(block的內部實現)
struct __block_impl {
  void *isa;//isa指向block的類
  int Flags;
  int Reserved;
  void *FuncPtr;//通過該指標實現呼叫
};

//__main_block_impl_0結構體中的第二個元素
static struct __main_block_desc_0 {
  size_t reserved;//預設是0
  size_t Block_size;// 描述bloc的大小
}

由上面可以看出:block是封裝了函式呼叫以及函式呼叫環境的OC物件。(是一個結構體並且有isa指標,所以是一個oc物件; block定義的結構體中__main_block_impl_0 裡面的FuncPtr指標是用來block呼叫的),其底層結構如下:
block底層結構

變數的捕獲

  • 為了保證block內部能夠正常訪問外部的變數,block有個變數捕獲機制
    變數捕獲機制
  • 變數一般分為全域性變數(函式外面宣告的變數,宣告以後的函式和外面都能訪問)和區域性變數(函式內部宣告的變數,只有函式內部能訪問),而區域性變數又分為自動區域性變數(auto修飾的,不寫預設就是自動變數,離開作用域就會被銷燬 )和靜態區域性變數(static修飾的,只建立一次,離開作用域不會被銷燬)。
    原檔案:
int a = 10;//全域性變數
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        auto int b = 20;//自動區域性變數
        static int c = 30;//靜態區域性變數
        void (^blocktest)(void) = ^{
            a = 40;
//            b = 50; 會報錯
            c = 60;
           NSLog(@"a : %d",a);
            NSLog(@"b : %d",b);
            NSLog(@"c : %d",c);
        };
        blocktest();
    }
    return 0;
}

編譯後的cpp檔案:

int a = 10;
//block 宣告
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *c;
  int b;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_c, int _b, int flags=0) : c(_c), b(_b) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//block 實現
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *c = __cself->c; // bound by copy
  int b = __cself->b; // bound by copy
            a = 40;
            (*c) = 60;
             NSLog((NSString*)&__NSConstantStringImpl__var_folders_w1_l3yxhrdj1210bnj2d6hrz4p00000gp_T_main_f85032_mi_0,a);
             NSLog((NSString*)&__NSConstantStringImpl__var_folders_w1_l3yxhrdj1210bnj2d6hrz4p00000gp_T_main_f85032_mi_1,b);      
             NSLog((NSString*)&__NSConstantStringImpl__var_folders_w1_l3yxhrdj1210bnj2d6hrz4p00000gp_T_main_f85032_mi_2,(*c));
//main 函式
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        auto int b = 20;
        static int c = 30;
        void (*blocktest)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &c, b));
        ((void (*)(__block_impl *))((__block_impl *)blocktest)->FuncPtr)((__block_impl *)blocktest);
    }
    return 0;
}

列印結果:
列印結果

  • 由上面可以看出:
  1. 如果是全域性變數,block裡面是可以直接訪問的,所以可以修改其值;
  2. 如果是自動區域性變數,在block 定義的時候內包會宣告一個同樣的名稱的變數,並將值賦值給該變數,所以此時無法訪問到外面的自動變數,故不能修改其值。(此時獲取該變數的值實際上是block內包的同名變數,不是外面的變數,可以通過列印地址驗證);
  3. 如果是靜態區域性變數,在block 定義的時候內包會宣告一個同樣的名稱的帶*號的變數,指向的是該變數的地址,可以通過地址來修改該變數的值。

注意:自動區域性變數是基本資料型別時是值傳遞,block內部不能訪問該,但是如果是OC物件變數,由於傳進去的是個物件即指標(*p),所以是可以直接訪問的。

block型別

  • block有3種類型,可以通過呼叫class方法或者isa指標檢視具體型別,最終都是繼承自NSBlock型別
    __NSGlobalBlock__ ( _NSConcreteGlobalBlock )
    __NSStackBlock__ ( _NSConcreteStackBlock )
    __NSMallocBlock__ ( _NSConcreteMallocBlock )
  • 首先看一下三種block的記憶體分配圖
    block的記憶體分配
  1. 程式區域局是放程式碼原碼,很小在最前面;
  2. 資料區域:主要儲存全域性變數;
  3. 堆:存放alloc出的物件,需要程式設計師自己申請和管理記憶體;
  4. 棧:儲存基本資料型別的區域性變數,會自動分配和釋放記憶體(作用域)。
  • 那麼如何區別一個block是那種型別呢,判定條件如下:
    block型別的判定條件
    示例:
int a = 1;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        void (^block1)(void) = ^{
            NSLog(@"a : %d",a);
        };
        int b = 2;
        void (^block2)(void)= ^ {
            NSLog(@"b : %d",b);
        };
         int c = 3;
        void (^block3)(void) = [^{
            NSLog(@"c : %d",c);
        } copy];
        
        block1();
        block2();
        block3();
        
        NSLog(@"block1父類的父類 : %@",[[[block1 class] superclass] superclass]);
        NSLog(@"block1 : %@",[block1 class]);
        NSLog(@"block2父類的父類  : %@",[[[block2 class] superclass] superclass]);
        NSLog(@"block2 : %@",[block2 class]);
        NSLog(@"block3 : %@",[block3 class]);
    }
    return 0;
}

列印結果:
block型別
結果可以看出:block2的型別應該是__NSStackBlock__的,但是為啥是__NSMallocBlock__呢?這是因為在ARC環境下,當訪問自動區域性變數時,block會自動進行copy操作。
該為mrc環境
改為MRC環境
列印結果:
MRC列印結果
MRC下block2的型別為__NSStackBlock__

  • 每一種型別的block呼叫copy後的結果如下所示
    block進行copy操作

  • 在ARC環境下,編譯器會根據情況自動將棧上的block複製到堆上,比如以下情況:
    block作為函式返回值時;
    將block賦值給__strong指標時;
    block作為Cocoa API中方法名含有usingBlock的方法引數時;
    block作為GCD API的方法引數時。

  • MRC下block屬性的建議寫法
    @property (copy, nonatomic) void (^block)(void);

  • ARC下block屬性的建議寫法
    @property (strong, nonatomic) void (^block)(void);
    @property (copy, nonatomic) void (^block)(void);

  • 為什麼當是基本資料型別區域性自動變數時,要進行copy呢?
    因為訪問基本資料型別區域性自動變數時,block是在棧上,將不會對auto變數產生強引用,這樣就有可能在block訪問auto變數前,變數就已經銷燬了。

  • 如果block被拷貝到堆上,會呼叫block內部的copy函數了,copy函式內部會呼叫_Block_object_assign函式;
    _Block_object_assign函式會根據auto變數的修飾符(__strong、__weak、__unsafe_unretained)做出相應的操作,形成強引用(retain)或者弱引用,預設是強引用。

  • 如果block從堆上移除,會呼叫block內部的dispose函式,dispose函式內部會呼叫_Block_object_dispose函式,
    _Block_object_dispose函式會自動釋放引用的auto變數(release)。
    在這裡插入圖片描述

  • 在使用clang轉換OC為C++程式碼時,可能會遇到以下問題
    cannot create __weak reference in file using manual reference

  • 解決方案:支援ARC、指定執行時系統版本,比如
    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-10.0.0 main.m

__block修飾符

  • 上面可以看出當時自動區域性變數(基本資料型別)時,block內部是不能修改該auto變數的值的(因為拿不到該變數),但是用__block 修飾該變數時就可以訪問該變量了。
    示例:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block auto int a = 10;
         NSLog(@"&a : %p",&a);
        void (^block)(void) = ^{
            a = 20;
            NSLog(@"a : %d",a);
            NSLog(@"&a : %p",&a);
        };
        block();
    }
    return 0;
}

編譯後cpp檔案

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

編譯器會將__block變數包裝成一個物件,其結構如下:
__block

  • __block的記憶體管理
    當block在棧上時,並不會對__block變數產生強引用;
    當block被copy到堆時;
    會呼叫block內部的copy函式;
    copy函式內部會呼叫_Block_object_assign函式;
    _Block_object_assign函式會對__block變數形成強引用(retain);
    一旦__block變數唄複製到堆中後,其他的block再訪問該變數時,該變數不會再一次複製到堆,也就是該__block變數只會被複制到堆上一次;

__block持有變數
當block從堆中移除時;
會呼叫block內部的dispose函式;
dispose函式內部會呼叫_Block_object_dispose函式;
_Block_object_dispose函式會自動釋放引用的__block變數(release)。
__block釋放變數

  • __block的__forwarding指標
    __forwarding
    由上面示意圖可以看出: 如果__block變數結構體只在棧中, __forwarding使用指向棧中的該變數結構體;如果複製到__block變數結構體複製到了堆中,這時棧和堆中都有該__block變數結構體,都有__forwarding指標,但是不管該__forwarding是在棧中還是在堆中,都會指向堆中的變數結構體,保證了唯一性。

迴圈引用問題

迴圈引用
當一個物件持有block,而block又持有該物件,這樣就產生的迴圈引用,就會造成這兩塊記憶體都沒法釋放,從而導致記憶體洩漏問題。

#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person * p = [[Person alloc]init];
        [p test];
    }
    return 0;
}

#import "Person.h"
@interface Person()
@property(nonatomic ,copy) void(^block)(void);
@end
@implementation Person
-(void)test{
    self.block = ^{
        NSLog(@"%@",NSStringFromClass([self class]));
    };
    self.block();
}
-(void)dealloc{
    NSLog(@"%s",__func__);
}
@end

上面示例可以看出:Person例項化時被p強引用了 Person 強引用了block,在block中訪問了self變數,所以又強應用了Person,當程式執行完,p這個指標不再指向Person初始化的這塊兒地址,但是block還在指向個Person例項物件,導致該Person物件無法釋放。

  • 解決方案:用__weak__unsafe_unretained解決
    __weak typeof(self)weakSelf = self;
    self.block = ^{
        NSLog(@"%@",NSStringFromClass([weakSelf class]));
    };
    或者
     __unsafe_unretained typeof(self)weakSelf = self;
    self.block = ^{
        NSLog(@"%@",NSStringFromClass([weakSelf class]));
    };

__weak__unsafe_unretained修飾self時,block就會弱引用Person,這時候當程式執行完p指標不在指向Person例項物件,block是弱引用,這時沒有強指標,所以該Person例項物件會銷燬,在銷燬前也會先銷燬block(進行一次release操作)。
其原理圖如下:
弱引用

  • 注意:self是一個隱示引數,每一個方法都含有self和SEL這兩個隱示引數,所以self是一個區域性變數;成員變數是儲存到物件中,訪問成員變數其實本質是通過self->成員變數,其實也訪問了self。
  • __weak__unsafe_unretained的區別:
    __weak 不會產生強引用,指向的物件銷燬時,會自動讓指標置為nil;
    __unsafe_unretained 不會產生強引用,不安全,指向的物件銷燬時,指標儲存的地址值不變(可能產生殭屍物件)。
  • 上面是ARC的情況,如果是MRC情況如何解決迴圈引用問題呢?
    使用__unsafe_unretained__block修飾self即可(同上),因為MRC沒有weak關鍵字,所以不能用__weak修飾。

block的具體使用

  • 宣告Block型別變數語法:

返回值型別 (^變數名)(引數列表) = Block表示式
示例:

int (^test)(int) = ^(int a){
    return  a + 1;
};
  • block作為屬性
typedef int (^Test)(int);
@interface Person : NSObject{
    int (^_a)(int);
    Test _test;
}
//block作為屬性
@property(nonatomic ,copy) int(^a)(int b);
@property(nonatomic ,copy) Test test;
@end

@implementation Person
- (void)setA:(int (^)(int))a{
    _a = a;
}
-(int (^)(int))a{
    return _a;
}
- (void)setTest:(Test)test{
    _test = test;
}
- (Test)test{
    return _test;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
//        NSLog(@"%d", test(10));
        Person * p = [[Person alloc]init];
        p.a = ^int(int a) {
            return a+1;
        };
        NSLog(@"%d", p.a(10));
    }
    return 0;
}
  • block作為方法引數
//還是給上面Person類新增的方法
- (void)funcWithBlock:(int (^)(int))parblock{
    NSLog(@"%d",parblock(100));
}
//呼叫
 [p funcWithBlock:^int(int a) {
            return a+1;
        }];
  • block作為方法的返回值
//block作為返回值
-(int (^)(int))returnBlockFunc{
    return ^(int count){
        return count+1000;
    };
}
//呼叫
int (^returnValue)(int) = [p returnBlockFunc];
NSLog(@"%d", returnValue(1000));
  • 利用typedef起別名簡化block
typedef int (^Test)(int);
//屬性
@property(nonatomic ,copy) Test test;
//引數
- (void)funcWithBlock:(Test)parblock;
//返回值
- (Test)returnBlockFunc;

面試題

  • block的原理是怎樣的?本質是什麼?
    封裝了函式呼叫以及呼叫環境的OC物件。
  • __block的作用是什麼?有什麼使用注意點?
    可以將區域性自動變數轉換為一個結構體,以達到在block內部訪問到該自動變數。如果是物件內向的區域性自動變數,因為本身就是指標型別,所以不需要用__block修飾就可以直接訪問。
  • block的屬性修飾詞為什麼是copy?使用block有哪些使用注意?
    block一旦沒有進行copy操作,就不會在堆上,注意迴圈引用問題。
    block在修改NSMutableArray,需不需要新增__block?
    不需要,因為NSMutableArray不是基本資料型別,在block中是指標傳遞。

相關推薦

iOS底層原理`OC語法`(Block)

block本質 對於iOS中的block很對人都會說是封裝了一塊程式碼或者是說就是一個程式碼塊,這種回答雖然是對的,但是很淺顯。那block究竟是個什麼東西呢?我們可以編譯後看一下底層的實現。 原檔案: 示例: int main(int argc, const

iOS底層原理總結--OC物件的分類:instance、class、meta-calss物件的isa和superclass

iOS底層原理總結–OC物件的本質(一) - 掘金 iOS底層原理總結–OC物件的本質(二) - 掘金 iOS底層原理總結–OC物件的分類:instance、class、meta-calss物件的isa和superclass - 掘金 iOS底層原理總結-- KVO/KVC的本質

iOS底層原理總結--OC物件的本質(二)

iOS底層原理總結–OC物件的本質(一) - 掘金 iOS底層原理總結–OC物件的本質(二) - 掘金 iOS底層原理總結–OC物件的分類:instance、class、meta-calss物件的isa和superclass - 掘金 iOS底層原理總結-- KVO/KVC的本質

iOS底層原理事件的傳遞與響應

iOS中的事件 iOS的事件分為3大型別:觸控事件、加速計事件、遠端控制事件;而我們最常用到的是觸控事件。 UIResponder(響應者物件) 在iOS中不是任何物件都能處理事件,只有繼承了UIResponder的物件才能接受並處理事件,我們稱之為“響應者物

iOS底層原理runloop

RunLoop簡介 從字面意思講跑圈,執行迴圈。RunLoop就是一個物件,這個物件管理了其需要處理的事件和訊息,並提供了一個入口函式來執行上面 Event Loop 的邏輯。執行緒執行了這個函式後,就會一直處於這個函式內部 “接受訊息->等待->

iOS底層原理多執行緒

簡介 進階 GCD 0GCD的常用函式: GCD中有2個用來執行任務的函式 用同步的方式執行任務 dispatch_sync(dispatch_queue_t queue, dispatch_block_t block); 用非同步的方式執行任務 di

iOS底層原理架構設計

何為架構? 架構(Architecture):軟體開發中的設計方案,類與類之間的關係、模組與模組之間的關係、客戶端與服務端的關係。 經常聽到的架構名詞:MVC、MVP、MVVM、VIPER、CDD、三層架構、四層架構等。 MVC - Apple版 Model

iOS底層原理總結 - 探尋block的本質(一)

release nss 命令 static 斷點 fix 生成 什麽是 block類型 面試題 block的原理是怎樣的?本質是什麽? __block的作用是什麽?有什麽使用註意點? block的屬性修飾詞為什麽是copy?使用block有哪些使用

[Java]I/O底層原理二:Socket工作機制

tcp連接 fin 連接建立 src 並發 如果 send rec 轉換 一、TCP狀態轉化 TCP連接的狀態轉換圖如下 註:SYN 表示建立鏈接、FIN 表示關閉鏈接、ACK 表示響應、PSH 表示有數據傳輸、RST 表示鏈接重置。 CLOSED:初始狀態,在超時或

理解數據庫連接池底層原理手寫實現

ring cda color 要去 分配 .com 管理 roc tex 前言 數據庫連接池的基本思想是:為數據庫連接建立一個“緩沖池”,預先在池中放入一定數量的數據庫連接管道,需要時,從池子中取出管道進行使用,操作完畢後,在將管道放入池子中,從而避免了頻繁的向數據庫申請資

小碼哥iOS底層原理班怎麽樣

ios高級 建議 由於 操作系統 1年 eas 小碼哥 學習過程 相關 眾所周知 小碼哥的iOS是不錯的,對於很多人來說自己從事iOS就是從小碼哥開始的,隨著發展現階段的iOS並不如之前那麽火熱了,那麽還有學習iOS的必要呢?回答是肯定的,一句老話:活到老學到老,人想要進步

iOS底層原理(二):Runtime研究(二)

這個篇幅我們繼續研究Runtime,這裡給大家介紹Runtime的API Working with Classes Adding Classes Instantiating Classes Working with Instances Obt

iOS底層面試題---OC語法部分

面試題的答案都是拋磚引玉,具體細節還得深入瞭解iOS底層原理 複製程式碼 1、一個NSObject物件佔用多少記憶體? 系統分配了16個位元組給NSObject物件(通過malloc_size函式獲得) 但NSObject物件內部只使用了8個位元組的空間(64bit環境下,可以通過class_ge

理解資料庫連線池底層原理手寫實現

前言 資料庫連線池的基本思想是:為資料庫連線建立一個“緩衝池”,預先在池中放入一定數量的資料庫連線管道,需要時,從池子中取出管道進行使用,操作完畢後,在將管道放入池子中,從而避免了頻繁的向資料庫申請資源,釋放資源帶來的效能損耗。在如今的分散式系統當中,系統的QPS瓶頸往往就

iOS底層原理總結-- 深入理解 KVC\KVO 實現機制

iOS底層原理總結–OC物件的本質(一) - 掘金 iOS底層原理總結–OC物件的本質(二) - 掘金 iOS底層原理總結–OC物件的分類:instance、class、meta-calss物件的isa和superclass - 掘金 iOS底層原理總結-- KVO/KVC的本質

[ios開發基礎]程式碼塊 ——block

iOS4引入了一個新特性,支援程式碼塊的使用, 這將從根本上改變你的程式設計方式。程式碼塊是對C語言的一個擴充套件,因此在Objective-C中完全支援。如果你學過Ruby,Python或Lisp程式設計 語言,那麼你肯定知道程式碼塊的強大之處。簡單的說,你可以通

iOS程式猿OC專案引入Swift方法

OC專案引入Swift方法 在OC專案中,有可能會遇到需要引入Swift寫的第三方庫,下面整理下OC專案如何使用Swift庫 1.在已有的OC工程中新建一個Swift檔案,命名為Test.swift,會彈出提示,選擇Create Bridging Header建立橋接檔案,

iOS底層原理班實戰視訊教程(上)-李明傑-專題視訊課程

iOS底層原理班實戰視訊教程(上)—448人已學習 課程介紹        iOS底層開發班實戰視訊培訓課程:APP逆向實戰、加殼脫殼、資料安全、編譯原理、iOS底層開發實現、iOS底層開發機制 iOS進階課程,實用技術不斷的更新和升級,更快幫助職場人士在開發領域脫穎而出。

關於iOS底層原理的若干解析

Linux程式設計點選右側關注,免費入門到精通!作者丨FindCrthttps://www.ji

PHP5底層原理垃圾回收機制

概念 垃圾回收機制 是一種記憶體動態分配的方案,它會自動釋放程式不再使用的已分配的記憶體塊。 垃圾回收機制 可以讓程式設計師不必過分關心程式記憶體分配,從而將更多的精力投入到業務邏輯。 與之相關的一個概念,記憶體洩露 指的是程式未能釋放那些已經不再使用的記憶體,造成記憶體的浪費。 那麼 PHP