技術分享-Runtime'黑魔法'

                                Make-by-LJW ---轉載請註明出處...
1、runtime簡介
  • RunTime簡稱運行時。OC就是運行時機制,也就是在運行時候的一些機制,其中最主要的是消息機制。(運行時機制,就是運行時作一些事情,消息機制,就是方法的調用... )
  • 對於C語言,函數的調用在編譯的時候會決定調用哪一個函數
  • 對於OC的函數,屬於動態調用過程,在編譯的時候並不能決定真正調用哪一個函數,只有在真正運行的時候纔會根據函數的名稱找到對應的函數來調用。
  • 事實證實:
    • 在編譯階段,OC能夠調用任何函數,即便這個函數並未實現,只要聲明過就不會報錯。
    • 在編譯階段,C語言調用未實現的函數就會報錯。

使用運行時步驟
    1.導入#import <objc/message.h>
    2.誰的事情,誰開頭
   
    runtime的消息機制函數,在xcode6以後就沒有提示參數
 
   怎麼才能讓消息機制函數有參數提示?
   點擊工程文件-> build Setting ->搜索msg ->不要嚴肅檢查消息機制調用面試

 

objc_getClass:獲取類對象
objc_getMetaClass:獲取元類
 
    OC基本常識:
    OC對象方法都保存在類對象中方法列表中,OC對象中類方法保存在元類(meta)
    OC中全部方法最終都會轉換成函數
 
    OC方法調用流程:
    [p eat]
    1.根據對象的isa去對應類對象去查找方法, isa:指向類對象
    2.根據傳入方法編號,去類對象中方法列表中查找對應方法
    3.調用方法實現
 
2、runtime做用
 
1.發送消息
  • 方法調用的本質,就是讓對象發送消息。
  • objc_msgSend,只有對象才能發送消息,所以以objc開頭.
  • 使用消息機制前提,必須導入#import <objc/message.h>
  • 消息機制簡單使用
  • clang -rewrite-objc main.m 查看最終生成代碼

 

// 建立person對象
    Person *p = [[Person alloc] init];

    // 調用對象方法
    [p eat];

    // 本質:讓對象發送消息

    objc_msgSend(p, @selector(eat));     SEL:方法編號 -> 尋找方法   // 調用類方法的方式:兩種
    // 第一種經過類名調用
    [Person eat];

    // 第二種經過類對象調用( 類方法調用本質:不是拿到類名調用,本質是拿到類對象)
    [[Person class] eat];

    // 用類名調用類方法,底層會自動把類名轉換成類對象調用
    // 本質:讓類對象發送消息
    objc_msgSend([Person class], @selector(eat));

消息機制原理:對象根據方法編號SEL去映射表查找對應的方法實現數組

  • OC對象方法保存到類對象中的方法列表中,OC對象

調用流程:
   // 1.首先獲取p對象的isa指針,就去isa指向類中查找
   // 2.根據傳入SEL找到對應方法名(函數入口)(這些方法實際上會轉換爲函數)
   // 3.直接調用函數實現xcode

2.交換方法
    • 開發使用場景:系統自帶的方法功能不夠,給系統自帶的方法擴展一些功能,而且保持原有的功能。
    • 方式一:繼承系統的類,重寫方法.
    • 方式二:使用runtime,交換方法.TODO: 在原來加載圖片功能之上,在添加一個判斷是否加載成功 
    •     1.自定義類,重寫系統方法 弊端:1.每次使用,都須要導入
          2.給UIImage提供分類,擴充方法 弊端:1.每次使用,都須要導入
          3.交換方法 交換imageNamed和xmg_imageNamed實現
          3.1 交換多少次 交換一次
          3.2 何時交換 一開始就交換好
       
          注意:不要在分類中,重寫系統方法實現,會致使系統方法被幹掉
      想給系統的方法,擴充,就能想到自定義類,重寫這個方法.
@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    // 需求:給imageNamed方法提供功能,每次加載圖片就判斷下圖片是否加載成功。
    // 步驟一:先搞個分類,定義一個能加載圖片而且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
    // 步驟二:交換imageNamed和imageWithName的實現,就能調用imageWithName,間接調用imageWithName的實現。
    UIImage *image = [UIImage imageNamed:@"ljw"];

}


@end

@implementation UIImage (Image)
// 加載分類到內存的時候調用
+ (void)load
{
    // 交換方法

    // 獲取imageWithName方法地址
    Method imageWithName = class_getClassMethod(self, @selector(imageWithName:));

    // 獲取imageWithName方法地址
    Method imageName = class_getClassMethod(self, @selector(imageNamed:));

    // 交換方法地址,至關於交換實現方式
    method_exchangeImplementations(imageWithName, imageName);

}

// 不能在分類中重寫系統方法imageNamed,由於會把系統的功能給覆蓋掉,並且分類中不能調用super.

// 既能加載圖片又能打印
+ (instancetype)imageWithName:(NSString *)name
{

    // 這裏調用imageWithName,至關於調用imageName
    UIImage *image = [self imageWithName:name];

    if (image == nil) {
        NSLog(@"加載空圖片");
    }

    return image;
}

@end

  • 交換原理:
    • 交換以前: app

    • 交換以後: 函數

3.動態添加方法

  • 開發使用場景:若是一個類方法很是多,加載類到內存的時候也比較耗費資源,須要給每一個方法生成映射表,能夠使用動態給某個類,添加方法解決。
  • 經典面試題:有沒有使用performSelector,其實主要想問你有沒有動態添加過方法。

 

4.給分類添加屬性
  • 原理:給一個類聲明屬性,其實本質就是給這個類添加關聯,並非直接把這個值的內存空間添加到類存空間。
  • 不能用靜態的,對象銷燬了屬性可能還在,致使內存泄露
  • 須要使用動態,保證對象不在,屬性就不在.
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    // 給系統NSObject類動態添加屬性  name

    NSObject *objc = [[NSObject alloc] init];
    objc.name = @"LJW";
    NSLog(@"%@",objc.name);

}

@end

// 定義關聯的key
static const char *key = "name";

@implementation NSObject (Property)

- (NSString *)name
{
    // 根據關聯的key,獲取關聯的值。
    return objc_getAssociatedObject(self, key);
}

- (void)setName:(NSString *)name
{
    // 第一個參數:給哪一個對象添加關聯

    // 第二個參數:關聯的key,經過這個key獲取(屬性名)
    // 第三個參數:關聯的value

    // 第四個參數:關聯的策略                              (strong)
    objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}
@end

5.字典轉模型

  • 設計模型:字典轉模型的第一步
    • 模型屬性,一般須要跟字典中的key一一對應
    • 問題:一個一個的生成模型屬性,很慢?
    • 需求:能不能自動根據一個字典,生成對應的屬性。
    • 解決:提供一個分類,專門根據字典生成對應的屬性字符串。

自動生成屬性代碼測試

@implementation NSDictionary (Code)
- (void)createPropertyCode
{
    // 根據字典中的key去生成對應屬性
    // 模型中屬性名 就是 key
    NSMutableString *codes = [NSMutableString string];
    
    // 遍歷字典
    [self enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull value, BOOL * _Nonnull stop) {
        NSString *code;
        // 生成一行屬性代碼
        if ([value isKindOfClass:[NSString class]]) {
            code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSString *%@;",key];
        } else if ([value isKindOfClass:[NSArray class]]) {
            code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSArray *%@;",key];
        } else if ([value isKindOfClass:[NSDictionary class]]) {
            code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSDictionary *%@;",key];
        } else if ([value isKindOfClass:NSClassFromString(@"__NSCFBoolean")]) {
            code = [NSString stringWithFormat:@"@property (nonatomic, assign) BOOL %@;",key];
        } else if ([value isKindOfClass:[NSNumber class]]) {
            code = [NSString stringWithFormat:@"@property (nonatomic, assign) int %@;",key];
            
        }
       
        // 拼接屬性代碼
        [codes appendFormat:@"\n%@\n",code];
        
    }];
    
    NSLog(@"%@",codes);
    
    
}
@end
  • 咱們都知道,字典轉模型基本的方式是:KVC
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.'
    // 獲取文件全路徑
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil];
    // 解析plist文件,生成字典對象
    NSDictionary *statusDict = [NSDictionary dictionaryWithContentsOfFile:filePath];

    // 設計模型,定義屬性
    // [statusDict createPropertyCode];
    
    // 字典轉模型
    StatusItem *item = [StatusItem statusWithDict:statusDict];
    
    NSLog(@"%@",item);
    
}

  • KVC字典轉模型弊端:必須保證,模型中的屬性和字典中的key一一對應。
    • 若是不一致,就會調用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:] 報key找不到的錯。
    • 分析:模型中的屬性和字典的key不一一對應,系統就會調用setValue:forUndefinedKey:報錯。
    • 解決:重寫對象的setValue:forUndefinedKey:,把系統的方法覆蓋, 就能繼續使用KVC,字典轉模型了。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key { }

  • 字典轉模型的方式二:Runtime
    • 思路:利用運行時,遍歷模型中全部屬性,根據模型的屬性名,去字典中查找key,取出對應的值,給模型的屬性賦值。
    • 步驟:提供一個NSObject分類,專門字典轉模型,之後全部模型均可以經過這個分類轉。
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    // 解析Plist文件
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil];

    NSDictionary *statusDict = [NSDictionary dictionaryWithContentsOfFile:filePath];

    // 獲取字典數組
    NSArray *dictArr = statusDict[@"statuses"];

    // 自動生成模型的屬性字符串
//    [NSObject resolveDict:dictArr[0][@"user"]];

    _statuses = [NSMutableArray array];

    // 遍歷字典數組
    for (NSDictionary *dict in dictArr) {

        Status *status = [Status modelWithDict:dict];

        [_statuses addObject:status];

    }

    // 測試數據
    NSLog(@"%@ %@",_statuses,[_statuses[0] user]);

}

@end

@implementation NSObject (Model)

+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    // 思路:遍歷模型中全部屬性-》使用運行時

    // 0.建立對應的對象
    id objc = [[self alloc] init];

    // 1.利用runtime給對象中的成員屬性賦值

    // class_copyIvarList:獲取類中的全部成員屬性
    // Ivar:成員屬性的意思
    // 第一個參數:表示獲取哪一個類中的成員屬性
    // 第二個參數:表示這個類有多少成員屬性,傳入一個Int變量地址,會自動給這個變量賦值
    // 返回值Ivar *:指的是一個ivar數組,會把全部成員屬性放在一個數組中,經過返回的數組就能所有獲取到。
    /* 相似下面這種寫法

     Ivar ivar;
     Ivar ivar1;
     Ivar ivar2;
     // 定義一個ivar的數組a
     Ivar a[] = {ivar,ivar1,ivar2};

     // 用一個Ivar *指針指向數組第一個元素
     Ivar *ivarList = a;

     // 根據指針訪問數組第一個元素
     ivarList[0];

     */
    unsigned int count;

    // 獲取類中的全部成員屬性
    Ivar *ivarList = class_copyIvarList(self, &count);

    for (int i = 0; i < count; i++) {
        // 根據角標,從數組取出對應的成員屬性
        Ivar ivar = ivarList[i];

        // 獲取成員屬性名
        NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];

        // 處理成員屬性名->字典中的key
        // 從第一個角標開始截取
        NSString *key = [name substringFromIndex:1];

        // 根據成員屬性名去字典中查找對應的value
        id value = dict[key];

        // 二級轉換:若是字典中還有字典,也須要把對應的字典轉換成模型
        // 判斷下value是不是字典
        if ([value isKindOfClass:[NSDictionary class]]) {
            // 字典轉模型
            // 獲取模型的類對象,調用modelWithDict
            // 模型的類名已知,就是成員屬性的類型

            // 獲取成員屬性類型
           NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
          // 生成的是這種@"@\"User\"" 類型 -》 @"User"  在OC字符串中 \" -> ",\是轉義的意思,不佔用字符
            // 裁剪類型字符串
            NSRange range = [type rangeOfString:@"\""];

           type = [type substringFromIndex:range.location + range.length];

            range = [type rangeOfString:@"\""];

            // 裁剪到哪一個角標,不包括當前角標
          type = [type substringToIndex:range.location];

            // 根據字符串類名生成類對象
            Class modelClass = NSClassFromString(type);

            if (modelClass) { // 有對應的模型才須要轉

                // 把字典轉模型
                value  =  [modelClass modelWithDict:value];
            }

        }

        // 三級轉換:NSArray中也是字典,把數組中的字典轉換成模型.
        // 判斷值是不是數組
        if ([value isKindOfClass:[NSArray class]]) {
            // 判斷對應類有沒有實現字典數組轉模型數組的協議
            if ([self respondsToSelector:@selector(arrayContainModelClass)]) {

                // 轉換成id類型,就能調用任何對象的方法
                id idSelf = self;

                // 獲取數組中字典對應的模型
                NSString *type =  [idSelf arrayContainModelClass][key];

                // 生成模型
               Class classModel = NSClassFromString(type);
                NSMutableArray *arrM = [NSMutableArray array];
                // 遍歷字典數組,生成模型數組
                for (NSDictionary *dict in value) {
                    // 字典轉模型
                  id model =  [classModel modelWithDict:dict];
                    [arrM addObject:model];
                }

                // 把模型數組賦值給value
                value = arrM;

            }
        }

        if (value) { // 有值,才須要給模型的屬性賦值
            // 利用KVC給模型中的屬性賦值
            [objc setValue:value forKey:key];
        }

    }

    return objc;
}

@end

寫在最後,runtime在開發中十分強大,是開發中的'黑魔法'ui

每每作到不少好用的功能,很強大!!!atom

相關文章
相關標籤/搜索