iOS runtime執行時的作用和應用場景

SionChen發表於2019-04-19

Runtime是什麼?

  眾所周知OC是一門高階程式語言,也是一門動態語言。有動態語言那也就有靜態語言,靜態語言---編譯階段就要決定呼叫哪個函式,如果函式未實現就會編譯報錯。如C語言。動態語言---編譯階段並不能決定真正呼叫哪個函式,只要函式宣告過即使沒有實現也不會報錯。如OC語言。   高階程式語言想要成為可執行檔案需要先編譯為組合語言再彙編為機器語言,機器語言也是計算機能夠識別的唯一語言,但是OC並不能直接編譯為組合語言,而是要先轉寫為純C語言再進行編譯和彙編的操作,從OC到C語言的過渡就是由runtime來實現的。然而我們使用OC進行物件導向開發,而C語言更多的是程式導向開發,這就需要將物件導向的類轉變為程式導向的結構體。   每當我面試的時候被問起Runtime相關知識的時候,總是隻能回答個大體內容,具體的應用場景也是說的三三兩兩,所以我感覺是時候總結一波Runtime的應用場景了。

Runtime應用場景

場景1--動態擴充套件屬性

  OC中類可以通過Category來直接擴充套件方法,但是卻不能直接通過新增屬性來擴充套件屬性(以我專案中用到的一個為例)。

#import <UIKit/UIKit.h>

@interface UIView (SPUtils)

@property(nonatomic)CALayer * shadowLayer;

@end
複製程式碼
#import "UIView+SPUtils.h"
#import <objc/runtime.h>
@implementation UIView (SPUtils)
-(void)setShadowLayer:(CALayer *)shadowLayer{
    objc_setAssociatedObject(self, @selector(shadowLayer), shadowLayer, OBJC_ASSOCIATION_RETAIN);
}
-(CALayer *)shadowLayer{
    return objc_getAssociatedObject(self, _cmd);
}

@end
複製程式碼

場景2--交換方法用於統一處理某個方法

  在iOS新發布的時候在Scrollview的頭部會系統預設多出一段空白,解決方法是設定其contentInsetAdjustmentBehavior屬性為UIScrollViewContentInsetAdjustmentNever。但對於現存的專案來說挨個修改工作量無疑是巨大的,也容易出問題。這時候就用到Runtime了,用runtime來交換其初始化方法來統一設定這個屬性就可以得到解決。

#import <UIKit/UIKit.h>

@interface UIScrollView (Inset)


@end

複製程式碼
#import "UIScrollView+Inset.h"
#import "CYXRunTimeUtility.h"

@implementation UIScrollView (Inset)
+(void)load{
    [CYXRunTimeUtility swizzlingInstanceMethodInClass:[self class] originalSelector:@selector(initWithFrame:) swizzledSelector:@selector(m_initWithFrame:)];
}
- (instancetype)m_initWithFrame:(CGRect)frame {
    
    UIScrollView *scrollV = [self m_initWithFrame:frame];
    if (@available(iOS 11.0, *)) {
        scrollV.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
    }
    return scrollV;
}
@end
複製程式碼

  實現交換方法的程式碼:

#import <Foundation/Foundation.h>

@interface CYXRunTimeUtility : NSObject
/**
 交換例項方法
 
 @param cls 當前class
 @param originalSelector originalSelector description
 @param swizzledSelector swizzledSelector description
 @return 返回
 */
+ (BOOL)swizzlingInstanceMethodInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector;


/**
 交換類方法

 @param cls 當前class
 @param originalSelector originalSelector description
 @param swizzledSelector swizzledSelector description
 @return 成
 */
+ (BOOL)swizzlingClassMethodInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector;

@end
複製程式碼
#import "CYXRunTimeUtility.h"
#import <objc/runtime.h>

@implementation CYXRunTimeUtility

+ (BOOL)swizzlingInstanceMethodInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
{
    Class class = cls;
    
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (didAddMethod)
    {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }
    else
    {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    return didAddMethod;
}

+ (BOOL)swizzlingClassMethodInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
{
    Class class = cls;
    
    Method originalMethod = class_getClassMethod(class, originalSelector);
    Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
    
    BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (didAddMethod)
    {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }
    else
    {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    return didAddMethod;
}

@end
複製程式碼

場景3--遍歷類屬性--對映解析

  開發日常中我們對網路請求下來的資料進行解析是必然的操作,包括很多三方解析框架都是通過runtime來獲取相關屬性進行對映解析的。   下面是我自己利用runtime獲取物件相關屬性並進行簡單深拷貝的例子(有不足之處,進攻參考):

#import <Foundation/Foundation.h>

@interface NSObject (MutableCopy)

-(id)getMutableCopy;

@end

複製程式碼
#import "NSObject+MutableCopy.h"

@implementation NSObject (MutableCopy)
-(id)getMutableCopy{
    NSArray * keys = [self getObjcPropertyWithClass:[self class]];
    id objc = [[[self class] alloc] init];
    for (NSString * key in keys) {
        if ([self valueForKey:key] == nil) continue;
        [objc setValue:[self valueForKey:key] forKey:key];
        //[objc setValue:[[self valueForKey:key] getMutableCopy] forKey:key];
    }
    return objc;
}

- (NSArray<NSString *> *)getObjcPropertyWithClass:(id )objc{
    //(1)獲取類的屬性及屬性對應的型別
    NSMutableArray * keys = [NSMutableArray array];
    NSMutableArray * attributes = [NSMutableArray array];
    /*
     * 例子
     * name = value3 attribute = T@"NSString",C,N,V_value3
     * name = value4 attribute = T^i,N,V_value4
     */
    unsigned int outCount;
    Class cls = [objc class];
    do {
        objc_property_t * properties = class_copyPropertyList(cls, &outCount);
        for (int i = 0; i < outCount; i ++) {
            objc_property_t property = properties[i];
            //通過property_getName函式獲得屬性的名字
            NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
            [keys addObject:propertyName];
            //通過property_getAttributes函式可以獲得屬性的名字和@encode編碼
            NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
            [attributes addObject:propertyAttribute];
        }
        //立即釋放properties指向的記憶體
        free(properties);
        cls = [objc superclass];
        objc = [cls new];
    } while ([NSStringFromClass([objc superclass]) isEqualToString:@"NSObject"]);
    return [keys valueForKeyPath:@"@distinctUnionOfObjects.self"];
}
@end
複製程式碼

場景4--修改isa指標,自己實現kvo

  物件導向中每一個物件都必須依賴一個類來建立,因此物件的isa指標就指向物件所屬的類根據這個類别範本能夠建立出例項變數、例項方法等。   Apple 使用了 isa 混寫(isa-swizzling)來實現 KVO 。當觀察物件A時,KVO機制動態建立一個新的名為:NSKVONotifying_A 的新類,該類繼承自物件A的本類,且 KVO 為 NSKVONotifying_A 重寫觀察屬性的 setter 方法,setter 方法會負責在呼叫原 setter 方法之前和之後,通知所有觀察物件屬性值的更改情況。   首先建立一個person類定義一個例項變數:

@interface Person : NSObject
{
    @public
    NSString * _name;
}

@property (nonatomic,copy) NSString *name;
@end
複製程式碼

建立一個NSObject的Category用於給所有NSObject及其子類新增 新增監聽方法:

@interface NSObject (KVO)
- (void)cyx_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
NSString * const cyx_key = @"observer";

@implementation NSObject (KVO)
-(void)cyx_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
    
    
    objc_setAssociatedObject(self, (__bridge const void *)(cyx_key), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    //修改isa 指標
    object_setClass(self, [SonPerson class]);
}

複製程式碼

  這裡利用runtime修改isa指標,修改呼叫方法時尋找方法的類。這裡我們修改到SonPerson類。並在SonPerson類裡面實現監聽方法。

extern NSString * const cyx_key;
@implementation SonPerson

-(void)setName:(NSString *)name{
    [super setName:name];
    
    NSObject * observer = objc_getAssociatedObject(self, cyx_key);
    
    [observer observeValueForKeyPath:@"name" ofObject:self change:nil context:nil];
}
複製程式碼

  這裡也用到了runtime 裡面 objc_getAssociatedObject 和objc_setAssociatedObject動態儲存方法。   好了那我們來用一下試一下效果吧。

#import "ViewController.h"
#import "Person.h"
#import "NSObject+KVO.h"
@interface ViewController ()

@property (nonatomic,strong) Person *p;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    Person * p = [[Person alloc] init];
    [p cyx_addObserver:self forKeyPath:@"name" options:0 context:nil];
    _p = p;
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",_p.name);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    static int i=0;
    i++;
    _p.name = [NSString stringWithFormat:@"%d",i];
    //_p -> _name = [NSString stringWithFormat:@"%d",i];
    
}
複製程式碼

輸出:

2018-04-21 11:15:17.974785+0800 04-響應式程式設計思想[1882:274712] 1
2018-04-21 11:15:18.293700+0800 04-響應式程式設計思想[1882:274712] 2
2018-04-21 11:15:18.687331+0800 04-響應式程式設計思想[1882:274712] 3
2018-04-21 11:15:19.036166+0800 04-響應式程式設計思想[1882:274712] 4
2018-04-21 11:15:19.396075+0800 04-響應式程式設計思想[1882:274712] 5
2018-04-21 11:15:19.699907+0800 04-響應式程式設計思想[1882:274712] 6
2018-04-21 11:15:19.981256+0800 04-響應式程式設計思想[1882:274712] 7

複製程式碼

  demo:github.com/SionChen/Re…

場景5--利用runtime實現訊息轉發機制的三次補救

  這個參考我的另一篇文章:www.jianshu.com/p/1073daee5…

總結

  當然runtime的強大不僅僅是隻能做這些事情,runtime還有很多用處等待我們大家去挖掘。

相關文章