你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS編程技術 >> iOS runtime的理解和應用

iOS runtime的理解和應用

編輯:IOS編程技術

項目中經常會有一些的功能模塊用到runtime,最近也在學習它.對於要不要閱讀runtime的源碼,我覺得僅僅是處理正常的開發,那真的沒有必要,只要把常用的一些函數看下和原理理解下就可以了.
但是如果真能靜下心好好閱讀源碼,真的能幫你更加深入理解objc本身以及經過高階包裝出來的那些特性。

什麼是runtime

runtime就是運行時,每個語言都有它的runtime.通俗點講就是程序運行時發生的事情.
比如C語言,在編譯的時候就決定了調用哪些函數,通過編譯後就一步步執行下去,沒有任何二義性,所以它是靜態語言.
而objc的函數調用則可以理解為發消息,在編譯的時候完全不能決定哪個函數執行,只有在運行的時候才會根據函數名找到函數調用,所以在運行的時候它能動態地添加調換屬性,函數.所以它是動態語言.
動態和靜態語言沒有明顯的界限,我感覺它們就是以runtime來區分的,看它在runtime時,有多靈活,那麼它就有多動態.

  • 相關定義

    typedef struct objc_method *Method
    struct objc_method {
    SEL method_name;            
    char *method_types;
    IMP method_imp;
    } 

    SEL是char*,可以理解為函數的姓名.
    IMP就是函數指針,指向函數的實現.
    ==在objc_class中method list保存了一個SEL<>IMP的映射.所以通過SEL可以找到函數的實現==

typedef struct objc_ivar *Ivar;
struct objc_ivar {
    char *ivar_name;
    char *ivar_type;
    int ivar_offset;
#ifdef __LP64__
    int space;
#endif
}                                                            

實例變量,跟某個對象關聯,不能被靜態方法使用,與之想對應的是類變量

typedef struct objc_category *Category;
struct objc_category {
    char *category_name;           
    char *class_name;
    struct objc_method_list *instance_methods;
    struct objc_method_list *class_methods;
    struct objc_protocol_list *protocols;
}      

Catagory可以動態地為已經存在的類添加新的行為。比如類方法,實例方法,協議.
==根據結構可知,不能添加屬性,實例變量==

struct objc_method_list {
    struct objc_method_list *obsolete;
    int method_count;
    int space;
    struct objc_method method_list[1];
}  

struct objc_ivar_list {
    int ivar_count;
    int space;
    struct objc_ivar ivar_list[1];
}                                                                    

==簡單地理解為存有方法和實例變量的數組==

//類在runtime中的表示
struct objc_class {
    Class isa;//指針,顧名思義,表示是一個什麼,
              //實例的isa指向類對象,類對象的isa指向元類

#if !__OBJC2__
    Class super_class;  //指向父類
    const char *name;  //類名
    long version;
    long info;
    long instance_size
    struct objc_ivar_list *ivars //成員變量列表
    struct objc_method_list **methodLists; //方法列表
    struct objc_cache *cache;//緩存
    //一種優化,調用過的方法存入緩存列表,下次調用先找緩存
    struct objc_protocol_list *protocols //協議列表
    #endif
};

struct objc_cache {
    unsigned int mask;
    unsigned int occupied;
    Method buckets[1];
};

==objc_cache可以理解為存最近調用過的方法的數組,每次調用先訪問它,提高效率==

runtime常用方法

  • 獲取列表
    我們可以通過runtime的一系列方法獲取類的一些信息(包括屬性列表,方法列表,成員變量列表,和遵循的協議列表)

    class_copyPropertyList       //獲取屬性列表
    class_copyMethodList         //獲取方法列表     
    class_copyIvarList           //獲取成員變量列表
    class_copyProtocolList       //獲取協議列表

    常見用於字典轉模型的需求中:

@interface LYUser : NSObject
@property (nonatomic,strong)NSString *userId;
@property (nonatomic,strong)NSString *userName;
@property (nonatomic,strong)NSString *age;
@end

- (void)viewDidLoad {    
    [super viewDidLoad];    
    //利用runtime遍歷一個類的全部成員變量
    NSDictionary *userDict = @{@"userId":@"1",@"userName":@"levi",@"age":@"20"};
    unsigned int count;
    LYUser *newUser = [LYUser new];
    objc_property_t *propertyList = class_copyPropertyList([LYUser class], &count);
    for (int i = 0; i < count; i++) {
        const char *propertyName = property_getName(propertyList[i]);
        NSString *key = [NSString stringWithUTF8String:propertyName];
        [newUser setValue:userDict[key] forKey:key];
    }
    NSLog(@"%@--%@--%@",newUser.userId,newUser.userName,newUser.age);     
}  

==這只是最簡單的轉化,還要考慮容錯,轉換效率,現在有很多開源框架做的很不錯.這是一些開源框架的性能對比:==模型轉換庫評測結果

  • 交換方法
class_getInstanceMethod() //類方法和實例方法存在不同的地方,所以兩個不同的方法獲得
class_getClassMethod()    //以上兩個函數傳入返回Method類型
method_exchangeImplementations    //()交換兩個方法的實現

==這個用到的地方很多,可以大大減少我們的代碼量,常用的有防錯措施,統計打點,統一更新界面效果==

防錯措施

-(void)viewDidLoad
{
    NSMutableArray *testArray = [NSMutableArray new];
    [testArray addObject:@"1"];
    NSString *a = nil;
    [testArray addObject:a];
    
    for (NSInteger i = 0; i < testArray.count; i++) {
        NSLog(@"%@",testArray[i]);
    }
}

@implementation NSMutableArray(ErrorLog)
+(void)load
{
    Method originAddMethod = class_getInstanceMethod(NSClassFromString(@"__NSArrayM"), @selector(addObject:));
    Method newAddMethod = class_getInstanceMethod(NSClassFromString(@"__NSArrayM"), @selector(el_addObject:));
    
    method_exchangeImplementations(originAddMethod, newAddMethod);
}

/*
 * 自己寫的方法實現
 */
-(void)el_addObject:(id)object
{
    if (object != nil) {
        [self el_addObject:object];
    }
    else
    {
        //可以添加錯誤日志
        NSLog(@"數組添加nil");
    }
}
@end  

統計打點

和上面的實現方式一致.在對應類的Category的load方法裡交換.

//  統計頁面出現
    Method originAddMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
    Method newAddMethod = class_getInstanceMethod([self class], @selector(el_ViewDidLoad));
    method_exchangeImplementations(originAddMethod, newAddMethod);

//  統計Button點擊
    Method originAddMethod = class_getInstanceMethod([self class], @selector(sendAction:to:forEvent:));
    Method newAddMethod = class_getInstanceMethod([self class],@selector(el_sendAction:to:forEvent:)));
    
    method_exchangeImplementations(originAddMethod, newAddMethod);

統一更新界面效果

很多時候我們做項目都是先做邏輯,一些頁面顏色,細節都是最後做.這就遇到了一些問題,可能只是改個cell右邊箭頭邊距,placeholder默認顏色.如果一個個改過來又麻煩又有可能有疏漏,這個時候runtime就可以大顯神通了.

//這個就可以統一cell右邊箭頭格式,非常方便
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(layoutSubviews);
        SEL swizzledSelector = @selector(swizzling_layoutSubviews);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

//設置cell右邊箭頭
- (void)setAccessoryType:(UITableViewCellAccessoryType)accessoryType {
    if (accessoryType == UITableViewCellAccessoryDisclosureIndicator) {
        UIImageView *accessoryView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"about_arrow_icon"]];
        accessoryView.centerY = self.centerY;
        accessoryView.right = self.width-16;
        self.accessoryView = accessoryView;
    } else if (accessoryType == UITableViewCellAccessoryNone) {
        self.accessoryView = nil;
    }
}

//設置cell右邊箭頭間距
- (void)swizzling_layoutSubviews {
    [self swizzling_layoutSubviews];
    if (self.imageView.image) {
        self.imageView.origin = CGPointMake(16, self.imageView.origin.y);
        self.textLabel.origin = CGPointMake(CGRectGetMaxX(self.imageView.frame)+10, self.textLabel.origin.y);
    } else {
        self.textLabel.origin = CGPointMake(16, self.textLabel.origin.y);
    }
    self.textLabel.width = MIN(self.textLabel.width, 180);
    self.accessoryView.right = self.width-16;
}  
  • 關聯對象
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

objc_getAssociatedObject(id object, const void *key)  

前面已經講過,Category不能添加屬性,通過關聯對象就可以在運行時動態地添加屬性.
這可是神器,對於封裝代碼很有用,例如很常見的,textField限制長度.每個都在delegate裡重復代碼肯定不行.自己寫個自定義textField,better,不過還是有點麻煩.而runtime就可以很優雅地解決問題.

.h
@interface UITextField (TextRange)
@property (nonatomic, assign) NSInteger maxLength;   //每次限制的長度設置下就行了
@end

.m
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)setMaxLength:(NSInteger)maxLength {
    objc_setAssociatedObject(self, KTextFieldMaxLength, @(maxLength), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    [self textField_addTextDidChangeObserver];
}

- (NSInteger)maxLength {
    return [objc_getAssociatedObject(self, KTextFieldMaxLength) integerValue];
}

#pragma mark - Private method
- (void)textField_addTextDidChangeObserver {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textField_textDidChange:) name:UITextFieldTextDidChangeNotification object:self];
}

#pragma mark - NSNotificationCenter action
- (void)textField_textDidChange:(NSNotification *)notification {
    UITextField *textField = notification.object;
    NSString *text = textField.text;
    MYTitleInfo titleInfo = [text getInfoWithMaxLength:self.maxLength];
    if (titleInfo.length > self.maxLength) {
        UITextRange *selectedRange = [textField markedTextRange];
        UITextPosition *position = [textField positionFromPosition:selectedRange.start offset:0];
        if (!position) {
            UITextRange *textRange = textField.selectedTextRange;
            textField.text = [textField.text subStringWithMaxLength:self.maxLength];
            textField.selectedTextRange = textRange;
        }
    }
}  

以上就是關於runtime最常用的介紹,我還在學習當中,會不停地完善,和大家分享進步.
最後給大家一個學習runtime的小技巧,畢竟看源碼真的很枯燥,可以去github上輸入import <objc/runtime.h>,就可以看到用到runtime的實例,使學習更有目標和動力.

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved