iOS - Runtime

来源:https://www.cnblogs.com/chenjiangxiaoyu/archive/2018/01/06/8206231.html
-Advertisement-
Play Games

一、Runtime簡介 1.1 簡單介紹 Runtime簡稱運行時。OC就是運行時機制,也就是在運行時候的一些機制,其中最主要的是消息機制; 對於C語言,函數的調用在編譯的時候會決定調用哪個函數; 對於OC的函數,屬於動態調用的過程,在編譯的時候並不能決定真正調用哪個函數,只有在真正運行的時候才會根 ...


一、Runtime簡介

1.1 簡單介紹

  • Runtime簡稱運行時。OC就是運行時機制,也就是在運行時候的一些機制,其中最主要的是消息機制;
  • 對於C語言,函數的調用在編譯的時候會決定調用哪個函數;
  • 對於OC的函數,屬於動態調用的過程,在編譯的時候並不能決定真正調用哪個函數,只有在真正運行的時候才會根據函數的名稱找到對應的函數來調用;
  • 在編譯階段,OC可以調用任何函數,即使這個函數並未實現,只要聲明過就不會報錯;在編譯階段,C語言調用未實現的函數就會報錯。

1.2 可以用來做什麼

  • 在程式運行的過程中,動態的創建一個類(比如KVO的底層實現)
  • 在程式運行過程中,動態地為某個類添加屬性/方法,可以用於封裝框架(想怎麼改就怎麼改),這也是我們runtime機制的主要運用方向
  • 遍歷一個類中所有的成員變數(屬性)/所有方法。比如字典轉模型:利用runtime遍歷模型對象的所有屬性,根據屬性名從字典中取出對應的值,設置到模型的屬性上;還有歸檔和解檔,利用runtime遍歷模型對象的所有屬性。

二、Runtime常用的方法

// 獲取類
Class PersonClass = object_getClass([Person class]);

// SEL是selector在Objc中的標示 method 是方法名
SEL oriSEL = @selector(method);

// 獲取類方法
Method oriMethod = class_getClassMethod(Class cls , SEL name);

// 獲取實例方法
Method insMethod = class_getInstanceMethod(Class cls, SEL name);

// 添加一個新方法
BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));

// 替換原方法實現
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));

// 交換兩個方法
method_exchangeImplementations(oriMethod, cusMethod);

// 獲取一個類的屬性列表(返回值是一個數組)
objc_property_t *propertyList = class_copyPropertyList([self class], &count);

// 獲取一個類的方法列表(返回值是一個數組)
Method *methodList = class_copyMethodList([self class], &count);

// 獲取一個類的成員變數列表 (返回值是一個數組)
Ivar *ivarList = class_copyIvarList([self class], &count);

// 獲取成員變數的名字
const char *ivar_getName(Ivar v)

// 獲取成員變數的類型
const char *ivar_getTypeEndcoding(Ivar v)

// 獲取一個類的協議列表(返回值是一個數組)
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);

// set方法
//將值value 跟對象object 關聯起來(將值value 存儲到對象object 中)
//參數 object:給哪個對象設置屬性
//參數 key:一個屬性對應一個Key,將來可以通過key取出這個存儲的值,key 可以是任何類型:double、int 等,建議用char 可以節省位元組
//參數 value:給屬性設置的值
//參數policy:存儲策略 (assign 、copy 、 retain就是strong)
void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy)

// get方法,利用參數key將對象object中存儲的對應值取出來
id objc_getAssociatedObject(id object , const void *key)

三、Runtime相關術語的數據結構

我感覺還是有必要看一下理論知識,如果不看跳過---

3.1 SEL

它是selector(方法選擇器)在Objc中的標示(Swift中是Selector類)。Objc在相同的類中不會有命名相同的兩個方法。selector對方法名進行包裝,以便找到對應的方法實現,它的數據結構:

// 它是個映射到方法的C語言字元串,可以通過Objc編譯器命令@selector 或者Runtime 系統的 sel_registerName 函數來獲取一個 SEL 類型的方法選擇器。
typedef struct objc_selector *SEL;

3.2 id

id 是一個參數類型,它是指向某個類的實例的指針。定義如下:

// 我們可以看到 objc_object 結構體包含一個isa指針,根據isa指針就可以找到對象所屬的類。
// isa 指針在代碼運行時並不總指向實例對象所屬的類型,所以不能依靠它來確定類型,要想確定類型還是需要用對象的 -class方法。
// KVO的實現原理就是將被觀察對象的isa指針指向了一個中間動態創建的中間類,而不是真實類型。
typedef struct objc_object *id; struct objc_object {Class isa;};

 3.3 Class

typedef struct objc_class *Class;

 Class其實是指向objc_class 結構體的指針。objc_class 的數據結構如下:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2_
    Class super_class// 父類            
    const char *name // 類名                                       
    long version     // 類的版本信息,預設為0
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 } OBJC2_UNAVAILABLE;

 從 objc_class 可以看到,一個運行時類中關聯了它的父類指針、類名、成員變數、方法、緩存以及附屬的協議。

其中 objc_ivar_list 和 objc_method_list 分別是成員變數列表和方法列表:

// 成員變數列表
struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
// 方法列表
struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;
    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}

 由此可見,我們可以動態修改 *methodList 的值來添加成員方法,這也是 Category 實現的原理,同樣解釋了 Category 不能添加屬性的原因。

objc_ivar_list 結構體用來存儲成員變數的列表,而 objc_ivar 則是存儲了單個成員變數的信息;同理,objc_method_list 結構體存儲著方法數組的列表,而單個方法的信息則由 objc_method 結構體存儲。

值得註意的是,objc_class 中也有一個 isa 指針,這說明 Objc 類本身也是一個對象。為了處理類和對象的關係,Runtime庫創建了一種叫做 Meta Class(元類) 的東西,類對象所屬的類就叫做元類。元類表述了類對象本身所具備的元數據。

我們所熟悉的類方法,就源自於 Meta Class。我們可以理解為類方法就是類對象的實例方法。每個類僅有一個類對象,而每個類對象僅有一個與之相關的元類。

當你發出一個類似 [NSObject alloc] (類方法)的消息時,實際上,這個消息被髮送給了一個類對象,這個類對象必須是一個元類的實例,而這個元類同時也是一個根元類的實例,所有元類的isa指針最終都指向根元類。

所以當 [NSObject alloc] 這條消息發送給類對象的時候,運行時代碼 objc_msgSend() 會去它元類中查找能夠響應消息的方法實現,如果找到了,就會對這個類對象執行方法調用。

3.4 Method

Method 是一個方法結構體的指針:

typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types    //  方法的返回值,和各個參數類型的字元串描述;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}

 objc_method 存儲了方法名,方法類型和方法實現:

  • 方法名類型為SEL
  • 方法類型 method_types 是個char指針,存儲方法的參數類型和返回值類型
  • method_imp 指向了方法的實現,本質是一個函數指針

3.5 Ivar

Ivar 是表示成員變數的類型。

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

其中,有成員變數的名稱,類型,空間, ivar_offset是基地址偏移位元組

 3.6 IMP

IMP在objc.h中的定義是:

typedef id (*IMP)(id, SEL,...);

 它就是一個函數指針,這是由編譯器生成的。當你發起一個ObjC消息之後,最終它會執行的那段代碼就是由這個函數指針指定的。而 IMP 這個函數指針就指向了這個方法的實現。

如果得到了執行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執行方法,這在後面Cache中會提到。

你會發現 IMP 指向的方法與 objc_msgSend 函數類型相同,參數都包含 id 和 SEL 類型。每個方法對應一個 SEL 類型的方法選擇器,而每個實例對象中的SEL對應的方法實現肯定是唯一的,通過id和 SEL 參數就能確定唯一的方法實現地址,而一個確定的方法也只有唯一的一組 id 和 SEL 參數。

3.7 Cache 

Cache 定義如下:

typedef struct objc_cache *Cache

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

 Cache 為方法調用的性能進行優化,每當實例對象接收到一個消息時,它不會直接在 isa 指針指向的類的方法列表中遍歷查找到能夠響應的方法,因為每次都要查找效率太低了,而是優先在 Cache 中查找。

Runtime 系統會把被調用的方法存到 Cache 中,如果一個方法被調用,那麼它有可能今後還會被調用,下次查找的收就會效率更高。

四、消息發送和消息轉發

4.1 消息發送

消息發送舉例:

[person read:book];

 會被編譯成:

objc_msgSend(person, @selector(read:), book);

 objc_msgSend的具體流程如下:

  • 通過 isa 指針找到所屬的類
  • 查找類的 cache 列表,如果沒有則下一步
  • 查找類的方法列表
  • 如果能找到與選擇子名稱相符的方法,就跳至其實現代碼
  • 找不到的話,就沿著繼承體系繼續向上查找
  • 如果能夠找到與選擇子名稱相符的方法,就調至其實現代碼
  • 如果還沒找到,就執行 “消息轉發”

4.2 消息轉發

這裡會給接收者最後一次機會把這個方法處理了,搞不定程式就會崩潰

- (void)forwardInvocation:(NSInvocation *)invocation
// invocation : 封裝了與那條尚未處理的消息相關的所有細節的對象

 在這裡能做的比較現實的事就是:在觸發消息前,先以某種方式改變消息內容,比如追加另外一個參數,或是改變消息等等。實現此方法時,如果發現某調用操作不應該由本類處理,可以調用超類的同名方法,則繼承體系中的每個類都有機會處理該請求,知道 NSObject ,如果NSObject 搞不定,則還會調用 doesNotRecognizeSelector: 來拋出異常,此時你就會在控制台看到熟悉的 unrecognized selector sent to instance..1

上面這4個方法均是模板方法,開發者可以override,由runtime來調用。最常見的實現消息轉發,就是重寫3和4。

五、Runtime的作用

5.1 發送消息

  • 方法調用的本質,就是讓對象發送消息;
  • objc_msgSend,只有對象才能發送消息,因此以objc開頭;
  • 使用消息機制的前提,必須導入 #import<objc/message.h>
  • 消息機制簡單使用
// 比如創建一個Person對象,我們通常會如下寫
    Person *p = [[Person alloc] init];
    
    // 其實方法的調用就是向這個對方發送消息,所以上面的代碼底層實現的主要代碼就是:
    // 由於iOS5以後,蘋果不支持這樣寫,所以需要在 Build Setting 中 將 Enable Strick Checking 設置為 NO
    Person *p = objc_msgSend(objc_msgSend(objc_getClass("Person"),sel_registerName("alloc")),sel_registerName("init"));

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

 

5.2 交換方法

簡單來說就是將兩個方法實現進行交換,比如系統自帶的方法功能不夠,我們想給系統自帶的方法擴展一些功能,並且保持原有的功能,我們就可以使用Runtie,交換方法。

比如:我們想在 NSMutableArray 添加新對象的時候,進行置空判斷。

// 需求:給 addObject: 方法提供功能,當添加一個新的對象時,進行為空判斷
    // 1 先弄個分類 定義一個 能添加新對象又能進行為空判斷的方法
    // 2 和系統的方法進行交換

#import <objc/message.h>
@implementation NSMutableArray (Category)
// 載入分類到記憶體的時候調用,會比main函數先調用
+(void)load{
    // 交換方法 獲取類名,如果使用[self class] 獲取的是 NSMutableArray ,因為NSMutableArray 是類簇,這個可以看看瞭解一下
    Class selfClass = NSClassFromString(@"__NSArrayM"); 
    // 獲取系統 addObject: 方法地址
    SEL sel_add = @selector(addObject:);
    Method addObject1 = class_getInstanceMethod(selfClass, sel_add);

    // 獲取自定義 safeAddObject: 方法地址
    SEL sel_safeAdd = @selector(safeAddObject:);
    Method safeAddObject2 = class_getInstanceMethod(selfClass, sel_safeAdd);

    // 然後交換方法地址,相當於交換實現方式
    method_exchangeImplementations(addObject1,safeAddObject2);
}

// 我們這裡定義一個新的添加對象的方法 , 不能在分類中重寫系統方法 addObject 這樣會覆蓋系統的addObject ,並且分類中也不能使用 super
- (void)safeAddObject:(id)anObject{
    
    // 我們可以加判斷,如果對象為空
    if (!anObject) {
        NSLog(@"對象為空,添加新對象失敗");
        return;
    }
    
    // 這裡調用 safeAddObject 其實是調用 addObject: 因為在load 方法中,系統的方法和你自己實現的方法已經交換了
    [self safeAddObject:anObject];
}

5.3 動態添加方法 

如果一個類方法非常多,載入類到記憶體的時候也比較耗費資源,需要給每個方法生成映射表,可以使用動態給某個類,添加方法解決。

NSMutableArray *mArr = [NSMutableArray array];
    // WCE_addObject 方法只是在分類中聲明,但是並沒有實現
    [mArr performSelector:@selector(WCE_addObject)];

// 現在我們在分類中預設去實現
/ 當對象調用一個未實現的對象方法,會進入這個方法,並把對應的方法列表傳過來

+(BOOL)resolveInstanceMethod:(SEL)sel{

    if (sel == @selector(WCE_addObject)) {

        // 動態添加 WCE_addObject 方法

        // 第一個參數:給哪個類添加方法

        // 第二個參數:添加方法的方法編號

        // 第三個參數:添加方法的函數實現(函數地址) 這裡需要轉換一下

        // 第四個參數:函數的類型,(返回值+參數類型) v:void @:對象-self SEL:_cmd

        class_addMethod(self, @selector(WCE_addObject), (void *)WCE_addObject, "v@:");

    }    

    return [super resolveInstanceMethod:sel];

}

// 預設方法都有兩個隱式參數 調用方法者 調用的方法

void WCE_addObject(id self,SEL sel){

    NSLog(@"這個方法被預設執行了");

}

 5.4 給分類添加屬性

給一個類聲明屬性,其實本質就是給這個類添加關聯,並不是直接把這個值的記憶體空間添加到類存空間。

比如我們想給NSMutableArr類動態添加一個wce_name屬性:

// wce_name 是動態的給NSMutableArray 添加的屬性
    NSMutableArray *mArr = [NSMutableArray array];
    mArr.wce_name = @"這是我的東西哦";
    NSLog(@"%@",mArr.wce_name);

// 下麵是具體實現
// 定義關聯的key
static const char *key = "wce_name";

@implementation NSMutableArray (Property)

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

-(void)setWce_name:(NSString *)wce_name{
    // 第一個參數:給哪個對象添加關聯
    // 第二個參數:關聯的key,通過這個key獲取
    // 第三個參數:關聯的value
    // 第四個參數:關聯的策略 retain copy strong
    objc_setAssociatedObject(self, key, wce_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

 5.5 獲取對象的所有屬性

比如我們想把一個字典轉化成對象,可以使用KVC,setValuesForKeysWithDictionary: ,但是必須要保證:模型中的屬性和字典中key一一對應,如果不一致,就會報錯。

所以,我們可以使用Runtime,遍歷對象中的所有屬性,根據模型的屬性名,去字典中查找key,取出對應的值,給模型的屬性賦值。建一個NSObject分類,專門字典轉模型,以後所有的模型都可以通過這個分類轉。

// 字典轉model
+ (instancetype)WCE_objectWithKeyValues:(NSDictionary *)dict{
    
    NSArray *propertyList = [self WCE_propertyList];
    
    id object = [[self alloc] init];
    
    for (NSString *key in propertyList) {
        if (dict[key]) {
            [object setValue:dict[key] forKey:key];
        }
    }
    return object;
}

// 獲取所有的屬性列表
+ (NSArray *)WCE_propertyList{
    NSMutableArray *mArr = [NSMutableArray array];
    
    // 獲取所有的屬性列表
    unsigned int count = 0;
    // 獲取各種所有成員屬性,無論是屬性還是成員變數。私有的也可以,這裡假設Person只有三個屬性
  // 如果只想獲取屬性可以用 class_copyPropertyList Ivar *ivarList = class_copyIvarList(self, &count); for (int i = 0; i < count; i++) { Ivar ivar = ivarList[i]; // 獲取成員屬性名 NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)]; // 因為獲取的是屬性,成員變數就是 _屬性名,這裡是去除下劃線 name = [name substringFromIndex:1]; // 添加到數組 [mArr addObject:name]; } return mArr; }

5.6 動態添加一個類

KVO(鍵值觀察)的實現就是利用的runtime能夠動態添加類

當你對一個對象進行觀察時,系統會自動新建一個類繼承自源類,然後重寫被觀察屬性的setter方法,然後重寫的setter方法會負責在調用原setter方法前後通知觀察者,然後把原對象的isa指針指向這個新類,我們知道,對象是通過isa指針去查找自己是屬於哪個類,並去所在類的方法列表中查找方法的,所以這個時候這個對象就自然地變成了新類的實例對象。

就像KVO一樣,系統是在程式運行的時候,根據你要監聽的類,動態添加一個新類繼承自該類,然後重寫原類的setter方法併在裡面通知observer的。

那麼,如何動態添加一個類呢?

// 創建一個類(size_t extraBytes該參數通常指定為0, 該參數是分配給類和元類對象尾部的索引ivars的位元組數。)
Class clazz = objc_allocateClassPair([NSObject class], "GoodPerson", 0);

// 添加ivar
// @encode(aType) : 返回該類型的C字元串
class_addIvar(clazz, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));

class_addIvar(clazz, "_age", sizeof(NSUInteger), log2(sizeof(NSUInteger)), @encode(NSUInteger));

// 註冊該類
objc_registerClassPair(clazz);

// 創建實例對象
id object = [[clazz alloc] init];

// 設置ivar
[object setValue:@"Tracy" forKey:@"name"];

Ivar ageIvar = class_getInstanceVariable(clazz, "_age");
object_setIvar(object, ageIvar, @18);

// 列印對象的類和記憶體地址
NSLog(@"%@", object);

// 列印對象的屬性值
NSLog(@"name = %@, age = %@", [object valueForKey:@"name"], object_getIvar(object, ageIvar));

// 當類或者它的子類的實例還存在,則不能調用objc_disposeClassPair方法
object = nil;

// 銷毀類
objc_disposeClassPair(clazz);

 

 六、項目中的具體使用

6.1 替換系統的方法

也就是將系統的方法和自己定義的方法替換,比如可變數組添加新對象、按標取值時的增加的防止崩潰的判斷,當我們接手一個新項目時,想更快瞭解界面跳轉,可以替換系統的viewWillAppear:方法。

6.2 實現分類也可以添加屬性

MJRefresh 就是動態的給UIScrollView增加的mj_header和mj_footer

6.3 字典和模型的自動轉換

其實主要的也是使用了,遍歷屬性這個方法。

6.4 動態的增加方法

動態的為某個類增加一個方法,可以對某一個功能,做擴展。

6.5 自動歸檔和自動解檔

我們假設有個Moviel類,有三個屬性 id name url ,通常在歸解檔的時候,我們都會

實現這兩個協議方法:

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:_movieId forKey:@"id"];
    [aCoder encodeObject:_movieName forKey:@"name"];
    [aCoder encodeObject:_pic_url forKey:@"url"];

}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super init]) {
        self.movieId = [aDecoder decodeObjectForKey:@"id"];
        self.movieName = [aDecoder decodeObjectForKey:@"name"];
        self.pic_url = [aDecoder decodeObjectForKey:@"url"];
    }
    return self;
}
@end

 如果這裡有100個屬性,那麼我們也只能把100個屬性都給寫一遍,

不過用runtime之後,就簡單了:

#import "Movie.h"
#import <objc/runtime.h>
@implementation Movie

- (void)encodeWithCoder:(NSCoder *)encoder

{
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([Movie class], &count);

    for (int i = 0; i<count; i++) {
        // 取出i位置對應的成員變數
        Ivar ivar = ivars[i];
        // 查看成員變數
        const char *name = ivar_getName(ivar);
        // 歸檔
        NSString *key = [NSString stringWithUTF8String:name];
        id value = [self valueForKey:key];
        [encoder encodeObject:value forKey:key];
    }
    free(ivars);
}

- (id)initWithCoder:(NSCoder *)decoder
{
    if (self = [super init]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([Movie class], &count);
        for (int i = 0; i<count; i++) {
        // 取出i位置對應的成員變數
        Ivar ivar = ivars[i];
        // 查看成員變數
        const char *name = ivar_getName(ivar);
       // 歸檔
       NSString *key = [NSString stringWithUTF8String:name];
      id value = [decoder decodeObjectForKey:key];
       // 設置到成員變數身上
        [self setValue:value forKey:key];

        }
        free(ivars);
    } 
    return self;
}
@end

 如果還嫌麻煩,我們可以把encodeWithCoder 和 initWithCoder 這兩個方法抽成巨集

#import "Movie.h"
#import <objc/runtime.h>

#define encodeRuntime(A) \
\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; i++) {\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [self valueForKey:key];\
[encoder encodeObject:value forKey:key];\
}\
free(ivars);\
\

#define initCoderRuntime(A) \
\
if (self = [super init]) {\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; i++) {\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [decoder decodeObjectForKey:key];\
[self setValue:value forKey:key];\
}\
free(ivars);\
}\
return self;\
\

@implementation Movie

- (void)encodeWithCoder:(NSCoder *)encoder

{
    encodeRuntime(Movie)
}

- (id)initWithCoder:(NSCoder *)decoder
{
    initCoderRuntime(Movie)
}
@end

 6.6 界面指定跳轉

在接收推送通知的時候,我們希望可以點擊這條通知跳轉到對應的頁面,簡單的方法就是,判斷,在判斷。但是我們可以利用runtime動態生成對象、屬性、方法這特性,我們可以先跟服務端商量好,定義跳轉規則,比如要跳轉到A控制器,需要傳屬性 id、type,那麼服務端返回字典給我,裡面有控制器名,兩個屬性名跟屬性值,客戶端就可以根據控制器名生成對象,再用kvc給對象賦值,這樣就搞定了。

比如,根據推送規則跳轉對應頁面 HSFeedsViewController

// 進入該頁面需要穿的屬性

@interface HSFeedsViewController : UIViewController
// 註:根據下麵的兩個屬性,可以從伺服器獲取對應的頻道列表數據
/** 頻道ID */
@property (nonatomic, copy) NSString *ID;
/** 頻道type */
@property (nonatomic, copy) NSString *type;
@end

 AppDelegate.m 中添加以下代碼片段:

// 這個規則肯定事先跟服務端溝通好,跳轉對應的界面需要對應的參數
NSDictionary *userInfo = @{
                           @"class": @"HSFeedsViewController",
                           @"property": @{
                                        @"ID": @"123",
                                        @"type": @"12"
                                   }
                           };

 接收到推送的消息後:

- (void)push:(NSDictionary *)params
{
    // 類名
    NSString *class =[NSString stringWithFormat:@"%@", params[@"class"]];
    const char *className = [class cStringUsingEncoding:NSASCIIStringEncoding];
    // 從一個字串返回一個類
    Class newClass = objc_getClass(className);
    if (!newClass)
    {
        // 創建一個類
        Class superClass = [NSObject class];
        newClass = objc_allocateClassPair(superClass, className, 0);
        // 註冊你創建的這個類
        objc_registerClassPair(newClass);
    }
    // 創建對象
    id instance = [[newClass alloc] init];
    // 對該對象賦值屬性
    NSDictionary * propertys = params[@"property"];
    [propertys enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        // 檢測這個對象是否存在該屬性
        if ([self checkIsExistPropertyWithInstance:instance verifyPropertyName:key]) {
            // 利用kvc賦值
            [instance setValue:obj forKey:key];
        }
    }];
    // 獲取導航控制器
    UITabBarController *tabVC = (UITabBarController *)self.window.rootViewController;
    UINavigationController *pushClassStance = (UINavigationController *)tabVC.viewControllers[tabVC.selectedIndex];
    // 跳轉到對應的控制器
    [pushClassStance pushViewController:instance animated:YES];
}

 // 檢查對象是否存在該屬性:

- (BOOL)checkIsExistPropertyWithInstance:(id)instance verifyPropertyName:(NSString *)verifyPropertyName
{
    unsigned int outCount, i;
    // 獲取對象里的屬性列表
    objc_property_t * properties = class_copyPropertyList([instance
                                                           class], &outCount);
    for (i = 0; i < outCount; i++) {
        objc_property_t property =properties[i];
        //  屬性名轉成字元串
        NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
        // 判斷該屬性是否存在
        if ([propertyName isEqualToString:verifyPropertyName]) {
            free(properties);
            return YES;
        }
    }
    free(properties); // 用copy的時候記得釋放
    return NO;
}

 

// 官方文檔翻譯

http://blog.csdn.net/coyote1994/article/details/52441513


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一個用c語言寫的程式把他編譯成電腦可執行的文件,一般有4個步驟 1,預處理:這個步驟,主要是包含頭文件,展開巨集定義 gcc -E helloworld.c -o helloworld.i 2,生成彙編代碼 gcc -S helloworld.i -o helloworld.s 3,編譯彙編 gcc ...
  • Ubuntu中安裝mysql5.7時沒有設置密碼,使用mysql -u root -p 不能連接mysql服務 在網上查有兩種方式: 1. 第一次安裝,mysql5.7設置了個預設密碼在某個目錄下存放,使用預設密碼登錄後,會提示修改密碼。這個方法暫時沒有嘗試 2.修改mysql的配置文件sudo v ...
  • 學習目標 -正則元字元串 -SQL語句中使用正則搜索 -SQL語句中使用正則匹配 -SQL語句中使用正則替代字元串 正則表達式 Oracle資料庫10g引入對正則表達式的支持。 實現了符合UNIX可移植操作系統(POSIX)標準,由電氣和電子工程師協會(IEEE)控制,ASCII數據匹配的語義和語法 ...
  • 轉載songdeyouxiang 1、資料庫命名規範採用26個英文字母(區分大小寫)和0-9的自然數(經常不需要)加上下劃線'_'組成;命名簡潔明確(長度不能超過30個字元);例如:user, stat, log, 也可以wifi_user, wifi_stat, wifi_log給資料庫加個首碼; ...
  • 作者:NiceCui 本文謝絕轉載,如需轉載需徵得作者本人同意,謝謝。 本文鏈接:http://www.cnblogs.com/NiceCui/p/8213723.html 郵箱:[email protected] 日期:2017 12 20 mysql安裝、配置 1. yum 下載mysql 僅限 ...
  • 當一張的數據達到幾百萬時,你查詢一次所花的時間會變多,如果有聯合查詢的話,我想有可能會卡在那兒了,那麼分表的目的就在於此,減小資料庫的負擔,縮短查詢時間。 ...
  • 最近項目上裝的mysql服務,分配的磁碟空間太小了,導致binlog兩天時間就能打滿,這裡記錄下處理方式 mysql的binlog日誌是一個很重要的日誌,以事件形式記錄了所有的DDL和DML(除了數據查詢語句)語句,還包含執行的消耗的時間,在數據丟失的緊急情況下,我們可以利用binlog日誌功能進行 ...
  • 學習目標 -理解分層查詢概念 -創建樹形組織報告 -格式化分層數據 -樹形組織排除分支 分層查詢 語法 SELECT [LEVEL],<column>,exper ... FROM <table_name> [WHERE condition(s)] START WITH condition(s) C ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...