iOS-分類Category詳解和關聯對象


 Category的實現原理

  • Category編譯之后的底層結構是struct category_t,里面存儲着分類的對象方法、類方法、屬性、協議信息
  • 在程序運行的時候,runtime會將Category的數據,合並到類信息中(類對象、元類對象中)

 

CategoryClass Extension的區別是什么?

  • Class Extension在編譯的時候,它的數據就已經包含在類信息中
  • Category是在運行時,才會將數據合並到類信息中

 

Category中有load方法嗎?load方法是什么時候調用的?load 方法能繼承嗎?

  • load方法
  • load方法在runtime加載類、分類的時候調用
  • load方法可以繼承,但是一般情況下不會主動去調用load方法,都是讓系統自動調用 (子類不會覆蓋父類的load方法 因為load是根據函數地址直接調用 而不是是通過objc_msgSend調用具體的可以看這個鏈接 里面有詳細的load 和 initialize的區別)

 

Category能否添加成員變量?如果可以,如何給Category添加成員變量?

  • 不能直接給Category添加成員變量,但是可以間接實現Category有成員變量的效果 實現方式
  1. 因為分類中的屬性只是生成了 set 和 get 方法的聲明,其實現和下划線變量都沒有生成 所以我們需要將為做的事情完成
    #import "MJPerson.h"
    
    @interface MJPerson (Test)
    //{
    //    int _weight;
    //}
    
    //分類里面聲明屬性 只是生成 屬性的set 和 get 方法 但是並沒有去實現和生成_變量
    @property (assign, nonatomic) int weight;
    
    
    @property (copy, nonatomic) NSString* name;
    
    //- (void)setWeight:(int)weight;
    //- (int)weight;
    
    @end
    #define MJKey [NSString stringWithFormat:@"%p", self]
    
    @implementation MJPerson (Test)
    
    NSMutableDictionary *names_;
    NSMutableDictionary *weights_;
    + (void)load
    {
        weights_ = [NSMutableDictionary dictionary];
        names_ = [NSMutableDictionary dictionary];
    }
    
    - (void)setName:(NSString *)name
    {
    //    NSString *key = [NSString stringWithFormat:@"%p", self];
        names_[MJKey] = name;
    }
    
    - (NSString *)name
    {
    //    NSString *key = [NSString stringWithFormat:@"%p", self];
        return names_[MJKey];
    }
    
    - (void)setWeight:(int)weight
    {
    //    NSString *key = [NSString stringWithFormat:@"%p", self];
        weights_[MJKey] = @(weight);
    }
    
    - (int)weight
    {
    //    NSString *key = [NSString stringWithFormat:@"%p", self];
        return [weights_[MJKey] intValue];
    }
    
    
    @end
    //用字典來實現有已下問題
    /*1.存儲的地方不一樣
    //person2.age = 20; // 20是存儲在peron2對象內部
    //person2.weight = 50; // 50是存放在全局的字典對象里面
    
     2.線程安全問題 (可以加鎖解決)
    */
    
    #import <Foundation/Foundation.h>
    #import "MJPerson.h"
    #import "MJPerson+Test.h"
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            MJPerson *person = [[MJPerson alloc] init];
            person.age = 10;
            person.weight = 40;
            
            
            MJPerson *person2 = [[MJPerson alloc] init];
            person2.age = 20; // 20是存儲在peron2對象內部
            person2.weight = 50; // 50是存放在全局的字典對象里面
            
            NSLog(@"person - age is %d, weight is %d", person.age, person.weight);
            NSLog(@"person2 - age is %d, weight is %d", person2.age, person2.weight);
        }
        return 0;
    }

     

  2. 使用關聯對象來實現 
    #import "MJPerson+Test.h"
    #import <objc/runtime.h>
    
    @implementation MJPerson (Test)
    
    - (void)setName:(NSString *)name
    {
    //   關聯對象 就是將傳進來的name 和 person對象(self) 關聯起來 ,達到 一個person對象對應一個name  就不用使用字典的方式來做了
        
    /*    參數講解
        <#id  _Nonnull object#>  你要給哪一個對象添加關聯對象 person對象(self)
     <#const void * _Nonnull key#> 關聯的key 取的時候需要用相當於內部應該有個字典
     <#id  _Nullable value#>  關聯對象是什么(關聯的值) name
     <#objc_AssociationPolicy policy#> 關聯策略
     objc_AssociationPolicy                   對應的修飾符
     OBJC_ASSOCIATION_ASSIGN                 assign
     OBJC_ASSOCIATION_RETAIN_NONATOMIC    strong, nonatomic
     OBJC_ASSOCIATION_COPY_NONATOMIC      copy, nonatomic
     OBJC_ASSOCIATION_RETAIN              strong, atomic
     OBJC_ASSOCIATION_COPY                copy, atomic
     
    //    objc_setAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>, <#id  _Nullable value#>, <#objc_AssociationPolicy policy#>)
     */
        objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    
    - (NSString *)name
    {
        // 隱式參數
        // _cmd == @selector(name)
        return objc_getAssociatedObject(self, _cmd);
    }
    
    - (void)setWeight:(int)weight
    {
        objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (int)weight
    {
        // _cmd == @selector(weight)
        return [objc_getAssociatedObject(self, _cmd) intValue];
    }
    
    //- (NSString *)name
    //{
    //    return objc_getAssociatedObject(self, @selector(name));
    //}
    //- (int)weight
    //{
    //    return [objc_getAssociatedObject(self, @selector(weight)) intValue];
    //}
    
    //#define MJNameKey @"name"
    //#define MJWeightKey @"weight"
    //- (void)setName:(NSString *)name
    //{
    //    objc_setAssociatedObject(self, MJNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    //}
    //
    //- (NSString *)name
    //{
    //    return objc_getAssociatedObject(self, MJNameKey);
    //}
    //
    //- (void)setWeight:(int)weight
    //{
    //    objc_setAssociatedObject(self, MJWeightKey, @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //}
    //
    //- (int)weight
    //{
    //    return [objc_getAssociatedObject(self, MJWeightKey) intValue];
    //}
    
    //static const void *MJNameKey = &MJNameKey;
    //static const void *MJWeightKey = &MJWeightKey;
    //- (void)setName:(NSString *)name
    //{
    //    objc_setAssociatedObject(self, MJNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    //}
    //
    //- (NSString *)name
    //{
    //    return objc_getAssociatedObject(self, MJNameKey);
    //}
    //
    //- (void)setWeight:(int)weight
    //{
    //    objc_setAssociatedObject(self, MJWeightKey, @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //}
    //
    //- (int)weight
    //{
    //    return [objc_getAssociatedObject(self, MJWeightKey) intValue];
    //}
    
    //static const char MJNameKey;
    //static const char MJWeightKey;
    //- (void)setName:(NSString *)name
    //{
    //    objc_setAssociatedObject(self, &MJNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    //}
    //
    //- (NSString *)name
    //{
    //    return objc_getAssociatedObject(self, &MJNameKey);
    //}
    //
    //- (void)setWeight:(int)weight
    //{
    //    objc_setAssociatedObject(self, &MJWeightKey, @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //}
    //
    //- (int)weight
    //{
    //    return [objc_getAssociatedObject(self, &MJWeightKey) intValue];
    //}
    
    @end
    #import <Foundation/Foundation.h>
    #import "MJPerson.h"
    #import "MJPerson+Test.h"
    #import <objc/runtime.h>
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            MJPerson *person = [[MJPerson alloc] init];
            person.age = 10;
            person.name = @"jack";
            person.weight = 30;
            
            
            MJPerson *person2 = [[MJPerson alloc] init];
            person2.age = 20;
            person2.name = @"rose";
            person2.name = nil;
            person2.weight = 50;
            
            NSLog(@"person - age is %d, name is %@, weight is %d", person.age, person.name, person.weight);
            NSLog(@"person2 - age is %d, name is %@, weight is %d", person2.age, person2.name, person2.weight);
        }
        return 0;
    }

     

關聯對象的原理

 

 

 

 

 

Category的底層結構

分類的本質(實現原理):

  • Category編譯之后的底層結構是 struct category_t (結構體),里面存儲着分類的對象方法、類方法、屬性、協議信息
  • 在程序運行的時候,runtime會將Category的數據,合並到類信息中(類對象、元類對象中)

分類底層結構定義的如下: 

//objc-runtime-new.h

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

 

項目中每定義一個分類,底層都會增加一個category_t對象。

Category源碼閱讀順序:

  1. objc-os.mm (runtime入口)
    • _objc_init (runtime初始化)
    • map_images
    • map_images_nolock
  2. objc-runtime-new.mm
    • _read_images
    • remethodizeClass
    • attachCategories
    • attachLists
    • realloc、memmove、 memcpy

category的加載過程:

  1. 通過runtime加載類的所有分類數據
  2. 將所有分類的方法,屬性,協議數據分別合並到一個數組 (后面參與編譯的Category數據,會在數組的前面)
  3. 將合並后的分類數據(方法,屬性,協議)插入到類原來到數據之前

由源碼可見,對同名方法而言,會優先調用分類中的方法。如果多個分類中包含同名方法,則會調用最后參與編譯的分類中的方法。

摘錄源碼中核心的attachCategories實現如下(objc4-756.2):

// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    //方法二維數組
    //[[method_t,method_t],[method_t,method_t,method_t]]
    //二維數組中的一個元素(數組)存放一個分類中的方法列表
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    //屬性二維數組
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    //協議二維數組
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    while (i--) {
        //取出分類
        auto& entry = cats->list[i];

        //取出分類中的方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }
    //取出(元)類對象中的數據(class_rw_t)
    auto rw = cls->data();
    
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    //將所有分類的方法添加到(元)類對象的方法列表中
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
    //將所有分類的屬性添加到(元)類對象的屬性列表中
    rw->properties.attachLists(proplists, propcount);
    free(proplists);
    //將所有分類的協議添加到(元)類對象的協議列表中
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

 

Category和Extension的區別

區別一

  • Category
    • 專門用來給類添加新的方法
    • 不能給類添加成員屬性(其實是可以通過runtime給分類添加屬性)
    • 分類中用@property定義變量,只會生成變量的getter、setter方法的聲明,不能生成方法實現和帶下划線的成員變量。
  • Extension
    • 可以說是特殊的分類,也稱作匿名分類
    • 可以給類添加成員屬性,但是是私有變量
    • 可以給類添加方法,也是私有方法

區別二

  雖然有人說Extension是一個特殊的Category,也有人將Extension成為匿名分類,但是兩者的區別很大。

  • Category

    • 是運行期決定的
    • 類擴展可以添加實例變量,分類不能添加實例變量(原因:因為在運行期,對象的內存布局已經確定,如果添加實例變量會破壞類的內部布局,這對編譯性語言是災難性的。)
  • Extension

    • 在編譯器決定,是類的一部分,在編譯器和頭文件的@interface和實現文件里的@implement一起形成了一個完整的類。
    • 伴隨着類的產生而產生,也隨着類的消失而消失。
    • Extension一般用來隱藏類的私有消息,必須有一個類的源碼才能添加一個類的Extension,所以對於系統的一個類,比如NSString,就無法添加類擴展。
 

1.Category 基本使用場合

  經常用的是給類添加方法,協議、屬性,編寫的分類里面的方法, 最終是在運行過程中的時候合並到類對象(對象方法)/元類對象(類方法)里面 (不是編譯的時候合並的)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM