初識 Runtime

前言

以前在看一些第三方源碼的時候,時不時的能碰到一些關於運行時相關的代碼。因而乎,就閱讀了一些關於運行時的文章,感受寫的都不錯,寫此篇文章爲了記錄一下,同時也從新學習一遍。app

Runtime簡介

  • Runtime簡稱運行時,OC就是運行時機制。
  • C語言中函數的調用在編譯的時候就會決定調用哪一個函數。
  • 對於OC來講,屬於動態調用過程,在編譯的時候並不能決定調用哪一個函數,只有真正運行的時候纔會根據函數的名稱找到對應的函數來調用。
  • 事實證實:
    1. 在編譯階段,OC能夠調用任何函數,即便這個函數並未實現,只要申明就不會報錯。
    2. 在編譯階段,C語言調用未實現的函數就會報錯。

Runtime的做用

發送消息

  • 方法調用的本質就是向對象發送消息。
  • objc_msgSend,只有對象才能發送消息,所以以objc開頭。注意:在oc中,不管是實例對象仍是Class,都是id類型的對象
  • 讓咱們來看看方法調用轉化成運行時的代碼,看看調用方法的真面目吧。函數

    1. 新建一個命令行工程
    2. 而後在main方法裏面寫上學習

      int main(int argc, const char * argv[]) {
          @autoreleasepool {
              NSObject *obj = [[NSObject alloc] init];
          }
          return 0;
      }
    3. 用終端跳轉到工程所在的根目錄,而後命令行運行clang -rewrite-objc main.m
    4. 而後ls查看一下當前目錄能夠看到有一個main.cpp文件,咱們用open main.cpp打開該文件,能夠看到一大串代碼,咱們能夠直接翻到底部,能夠看到這樣的代碼:atom

      int main(int argc, const char * argv[]) {
          /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
                  NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
              }
              return 0;
      }
    5. 刪除掉一些強制轉換,將上面的代碼簡化後:spa

      int main(int argc, const char * argv[]) {
          /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
                  NSObject *obj = objc_msgSend(objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
              }
          return 0;
          }

總結:到這裏,咱們能夠看到調用方法的本質就是發送消息了,而且能夠看到咱們寫的命令行

NSObject *obj = [[NSObject alloc] init];

上面這條語句發送了兩次消息,第一次發送了alloc消息,第二次發送了init消息。code

給分類添加屬性

相信你們都知道分類是不能夠添加屬性的,不過咱們能夠經過運行時,給分類動態的添加屬性。orm

原理:給一個分類聲明屬性,其本質就是給這個類添加關聯,並非直接把這個值的內存空間添加到類存空間。對象

實踐

  • 咱們給NSObject添加一個分類,而後聲明一個name屬性。繼承

    #import <Foundation/Foundation.h>
        @interface NSObject (Extension)
        @property (nonatomic, copy) NSString *name;
        @end
  • 咱們建立一個NSObject對象,而後給name屬性賦值,而且打印name的值。
    咱們能夠看到編譯成功,可是運行的時候就會華麗麗的崩潰,這就是所謂的不能給分類添加屬性的緣由了。不過咱們能夠經過運行時實現。
  • 接下來咱們在.m文件重寫name屬性的setter以及getter方法。

    #import "NSObject+Extension.h"
        #import <objc/runtime.h>
        static const char *key = "name";
        @implementation NSObject (Extension)
        -(NSString *)name {
            // 根據關聯的key,獲取關聯的值
            return objc_getAssociatedObject(self, key);
        }
        -(void)setName:(NSString *)name {
            // 第一個參數:給哪一個對象添加關聯
            // 第二個參數:關聯的key,經過這個key獲取
            // 第三個參數:關聯的value
            // 第四個參數:關聯的策略
            objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
        }
        @end
  • 運行程序,編譯成功,運行也成功。

動態添加方法

  • 開發使用場景: 若是一個類方法很是多,加載該類到內存的時候比較耗費資源,須要給每一個方法生成映射表,可使用運行時給該類動態添加方法來解決。(ps:我感受這個在開發中不經常使用)

實踐

  • 建立一個繼承NSObjectStudent類,聲明一個study方法。

    #import <Foundation/Foundation.h>
        @interface Student : NSObject
        -(void)study;
        @end
  • 建立一個Student對象,調用study方法。

    Student *s = [[Student alloc] init];
       [s performSelector:@selector(study)];

    此時會出現一個經典的報錯:
    objc -[Student study]: unrecognized selector sent to instance 0x7fd719cbb2f0

    錯誤緣由:調用一個未實現的實例方法

  • 咱們在Student類.m文件動態添加study方法的實現。

    #import "Student.h"
        #import <objc/runtime.h>
        @implementation Student
        // void(*)()
        // 默認方法都有兩個隱式參數,
        void studyStudent(id self, SEL sel){
            NSLog(@"%@--%@",self,NSStringFromSelector(sel));
        }
        // 當一個對象調用未實現的方法,會調用該方法處理,而且會把對應的方法列表傳進來,咱們能夠在這個方法裏判斷,未實現的方法是否是咱們想要動態添加的方法。
        +(BOOL)resolveInstanceMethod:(SEL)sel {    
            if (sel == @selector(study)) {
                // 第一個參數:給哪一個類添加方法
                // 第二個參數:添加方法的方法編號
                // 第三個參數:添加方法的函數實現(函數地址)
                // 第四個參數:函數的類型,(返回值+參數類型) v:void @:對象->self :表示SEL->_cmd
                class_addMethod(self, @selector(study), studyStudent, "v@:");
                // 注意:此處須要立刻結束此方法(不然,若是存在繼承關係的話,會調用到父類去)
                return YES;
            }
            return [super resolveInstanceMethod:sel];
        }
        @end

    交換方法實現

  • 交換方法實現,也就是所謂的Method Swizzling
  • 場景一:若是你整個項目都作完了,而後產品經理告訴你想統計每個頁面停留的時長。
  • 場景二:系統自帶的方法功能不夠,給系統自帶的方法擴展一些功能,而且保持原有功能。

實踐

場景一:統計整個項目每個頁面停留時長。(解決方法也不是惟一的)

  • 方式一:找到全部的控制器,而後寫上:

    -(void)viewWillAppear:(BOOL)animated {
           [super viewWillAppear:animated];
           [MobClick beginLogPageView:NSStringFromClass([self class])];
        }
        -(void)viewWillDisappear:(BOOL)animated {
           [super viewWillDisappear:animated];
           [MobClick endLogPageView:NSStringFromClass([self class])];
        }

    而後咱們一個項目可能有幾十個甚至上百個頁面須要統計,咱們總不可能每一個頁面都這樣寫吧。因而乎,就有了方式二

  • 方式二:全部的界面都繼承於一個基類,而後在基類中寫上

    -(void)viewWillAppear:(BOOL)animated {
           [super viewWillAppear:animated];
           [MobClick beginLogPageView:NSStringFromClass([self class])];
        }
        -(void)viewWillDisappear:(BOOL)animated {
           [super viewWillDisappear:animated];
           [MobClick endLogPageView:NSStringFromClass([self class])];
        }

    但是一開始寫項目的時候,並無使用到繼承,因此又papapa地就整個項目的控制器都繼承於一個基類,重複地將每個控制器的繼承都該成了咱們建立的基類。可是,這樣解決真的好麼,有可能咱們有些界面是繼承自UITableViewController的,UICollectionViewController,等等。那麼你就可能會對這些控制器再單獨的寫上面的代碼了。

    好不容易將整個項目改過來了,而後某天,公司來了一位新人,你告訴他全部的類都要繼承自你寫的那個基類,新手老是會不經意地犯錯誤(也有多是人家尚未習慣),有些類忘記繼承了,後期排查起來費力費時。那麼有沒有更好地解決方式呢?方式三就能夠處理這種問題。

  • 方式三:使用Method Swizzling實現,給UIViewController寫一個分類。

    #import "UIViewController+Help.h"
        #import <objc/runtime.h>
        @implementation UIViewController (Help)
        +(void)load {
            static dispatch_once_t onceToken;
            dispatch_once(&onceToken, ^{
                Class class = [self class];
                methodSwizzling(class, @selector(viewWillAppear:), @selector(scott_viewWillAppear:));
                methodSwizzling(class, @selector(viewWillDisappear:), @selector(scott_viewWillDisappear:));
            });
        }
        void methodSwizzling(Class class, SEL originSelector, SEL swizzSelector){
            Method originMethod = class_getInstanceMethod(class, originSelector);
            Method swizzMethod = class_getInstanceMethod(class, swizzSelector);
            BOOL isAddMethod = class_addMethod(class, originSelector, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
            if (isAddMethod) {
                class_replaceMethod(class, swizzSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
            }else{
                method_exchangeImplementations(originMethod, swizzMethod);
            }
        }
        -(void)scott_viewWillAppear:(BOOL)animated {
            [self scott_viewWillAppear:animated];
            NSLog(@"調用自定義viewWillAppear");
        }
        -(void)scott_viewWillDisappear:(BOOL)animated {
            [self scott_viewWillDisappear:animated];
            NSLog(@"調用自定義viewWillDissappear");
        }
        @end

字典轉模型

這個單獨開一篇給你們講講吧。

結束語

但願經過本文能讓你們學習到一些關於Runtime的知識,若是有什麼疑問,歡迎你們一塊兒討論。

相關文章
相關標籤/搜索