老生常談category增加屬性的幾種操作

JungHsu發表於2017-12-07

前言

日常開發中,為一個已有的類(比如說不想影響其檔案結構)、第三方庫提供的類增加幾個property,已經是十分常見且需要的操作了,有人會單獨起草一份category.m檔案,也有人直接繼承,像我一般會用category,一是能減少類檔案的數量提高編譯速度,二也是為了程式碼結構更加清晰。

這篇文章是用來寫Category的進行屬性擴充套件的行為的,所以我還是言歸正傳,首先,我要闡述一下目前比較主流的幾個屬性擴充套件形式,再往下進行分析:

  1. 利用 objc_setAssociatedObject函式進行物件的聯合。
  2. 利用 class_addProperty 函式進行類屬性的擴充套件
  3. 通過內部建立一個其他物件(比如字典),通過重寫本物件set和get或者訊息轉發。

下面對這三種常用方法進行分析,其實常見的都是前面兩種,第三種也是比較非主流。在分析這三種之前,我要談一下為什麼不能用 class_addIvar 函式。

  • class_addIvar 函式

在蘋果文件中,對 class_addIvar 函式有下面一段話:

This function may only be called after objc_allocateClassPair(_:_:_:) and before objc_registerClassPair(_:). Adding an instance variable to an existing class is not supported.
The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.

這個功能只能在 objc_allocateClassPair(_:_:_:) 之後和 objc_registerClassPair(_:) 之前呼叫。不支援將例項變數新增到現有的類。
該類不能是元類。不支援將例項變數新增到元類。
複製程式碼

文件是說不能將此函式用於已有的類,必須是動態建立的類,為了能夠知道為何會這樣,我們需要翻閱一下蘋果開源的 runtime 原始碼。

  1. 首先看一下關於 objc_allocateClassPair 函式的程式碼實現:

去除干擾程式碼,我們尋找到下面的函式呼叫鏈條:
objc_allocateClassPair -> objc_initializeClassPair_internal

// 下面的程式碼已經被我大部分剔除,只留下我們分析所需要用到的程式碼
static void objc_initializeClassPair_internal(Class superclass, const char *name, Class cls, Class meta)
{
    // Set basic info

    cls->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING;
    meta->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING;
    cls->data()->version = 0;
    meta->data()->version = 7;
    
    // RW_CONSTRUCTING 類已分配但還未註冊
    // RW_COPIED_RO class_rw_t->ro 來自 class_ro_t 結構的複製
    // RW_REALIZED //  class_t->data 的結構為 class_rw_t
    // RW_REALIZING // 類已開始分配,但並未完成
    // 以上幾個巨集都是對新類的class_rw_t結構設定基本資訊
}

複製程式碼
  1. 下面是class_addIvar的與我分析所需要的實現程式碼
// 無關程式碼已經剔除
BOOL 
class_addIvar(Class cls, const char *name, size_t size, 
              uint8_t alignment, const char *type)
{
    if (!cls) return NO;

    if (!type) type = "";
    if (name  &&  0 == strcmp(name, "")) name = nil;

    rwlock_writer_t lock(runtimeLock);

    assert(cls->isRealized());

    // No class variables
    if (cls->isMetaClass()) {
        return NO;
    }

    // Can only add ivars to in-construction classes.
    if (!(cls->data()->flags & RW_CONSTRUCTING)) {
        return NO;
    }

}
// 重點在這最後一句,前面我們已經看到 objc_allocateClassPair 函式所分配的新類的flags位資訊,在此處 & RW_CONSTRUCTING,必定為真,取反後跳過大括號向下執行。

複製程式碼
  1. 已經存在的類,經過測試,flag位為 RW_REALIZED|RW_REALIZING,設定函式如下:
static Class realizeClass(Class cls)
{
    runtimeLock.assertWriting();

    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;
    
    if (!cls) return nil;
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));

    // fixme verify class is not in an un-dlopened part of the shared cache?

    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        cls->setData(rw);
    }
}

所以在經過條件 !((RW_REALIZED | RW_REALIZING) & RW_CONSTRUCTING) 時返回NO。
複製程式碼

以上便是對已有類不能使用 class_addIvar 函式的分析

好了,回到真正的話題,對上面三種操作的分析:

  • objc_setAssociatedObject

我們都知道,在category中使用property,可以生成set和get的方法宣告,原因在此不做分析,一般為了方便的呼叫,我們都會寫上property,關鍵在於沒有set和get的實現,於是就會有下面這樣的程式碼:

static void *key = "key";
@implementation Person (Extra)

// 此處不考慮讀寫鎖的問題
- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name{
    return objc_getAssociatedObject(self, key);
}
@end
複製程式碼

上面的 objc_setAssociatedObject 函式內部的呼叫鏈條如下:

objc_setAssociatedObject -> objc_setAssociatedObject_non_gc -> _object_set_associative_reference

// 其中主要操作都在 _object_set_associative_reference 函式中,內部實現類似一般屬性的set實現(保留新值,釋放舊值),在此我們不進行深究,具體可以參考業內大佬的部落格文章。
複製程式碼

這種操作很直觀的表達了我們的需要,且API十分友好,僅僅是對於 weak 策略我們需要自己設計一個。

並且這種操作的好處是我們無需關係關聯物件的宣告週期,因為和普通的屬性一樣,會隨著宿主物件的釋放而釋放,具體可以看以下程式碼:

dealloc -> _objc_rootDealloc -> rootDealloc -> object_dispose -> objc_destructInstance
// 大部分釋放操作在 objc_destructInstance 函式中完成

// 下面是 objc_destructInstance 函式的實現程式碼
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = !UseGC && obj->hasAssociatedObjects();
        bool dealloc = !UseGC;

        // This order is important.
        // 內部通過C++的解構函式進行物件屬性的釋放,具體可看sunny大神的博文
        if (cxx) object_cxxDestruct(obj);
        // 此處會移除所有的關聯物件,也就是objc_setAssociatedObject 函式所設定上去的物件
        if (assoc) _object_remove_assocations(obj);
        // 清空引用計數與weak表
        if (dealloc) obj->clearDeallocating();
    }

    return obj;
}
複製程式碼

當然也有不足之處,利用 objc_setAssociatedObject 生成的關聯物件無法直接利用目前主流的Json轉Model庫(原因是無法在ivar及property中遍歷出來)。

  • 利用 class_addProperty 函式進行類屬性的擴充套件

class_addProperty 函式可以為我們生成類的property,@property是編譯器的識別符號,在普通類中可生成property、ivar、setMethod與getMethod,在我看來property的真實作用類似於方法的宣告,後面我會再談為什麼。

在分類中使用class_addProperty和普通類一樣, 只能生成set和get方法的宣告,無論有沒有被實現,我們都可以用 class_copyMethodList 函式得到property的list,如果這時候你想儲存屬性值,你依然必須手動或動態實現那些set和get方法,並且真實資料的儲存也必須由你自己提供實現,比如可以使用前面所說的objc_setAssociatedObject 函式。

現在說說為啥property只是一個類似宣告的作用呢,我們可以從蘋果開源的程式碼中找到蛛絲馬跡:

Class 是一個指向結構體 objc_class 的指標,而此結構體的結構如下所示:
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;  // 指向父類
    cache_t cache;             // 快取指標與vtable(沒學過C++,沒了解過虛擬函式這些),加速方法的呼叫
    class_data_bits_t bits;   // 真正儲存物件的ivar,property與method等資訊的地方
    }
    
    在原始碼中大部分時候表現為將類的大部分資訊儲存在 class_rw_t *rw指標中,不過內部也是返回bits中處理後的資訊
    
        class_rw_t *data() { 
        return bits.data();
    }
    
    在class_rw_t的結構中,結構如下所示:
    struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;   // 類的資訊標記
    uint32_t version; // 當前執行時版本

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    
    }
    

    
複製程式碼

可以看到在class_rw_t的結構中,包含了另一個十分相似的 const class_ro_t *ro 成員變數。

這個成員變數為一個不可修改內容的結構體指標,其中儲存了類在編譯時就已經確定好的ivar、 property、method、protocol等資訊,在類的初始化時會通過 methodizeClass 函式將其大部分內容都拷貝到 class_rw_t *rw中,其中 ivar 不會被拷貝,這也是前面所說的不能在執行時給已有的類增加 ivar的原因。

像property、method、protocol都是可以在執行時動態新增的,且儲存到 rw 的結構中去。

好像說的有點跑題了,我們們還是一起看看property到底儲存了什麼資訊:

struct property_t {
    const char *name;
    const char *attributes;
};
複製程式碼

可以看到,propperty中並沒有儲存很多資訊,只有name和配置的屬性,也沒有實現函式的地址,所以前面我說property的作用其實和方法的宣告是差不多的。

關於property的好處,也就是在使用網上json轉model庫時可以被遍歷到了,但是如果你沒有實現set和get,那依然會導致KVC的crash。

  • 通過內部建立一個其他物件(比如字典),通過重寫本物件set和get或者訊息轉發。

最後一種方法,也是比較少用的方式,說起來也比較簡單,比如定義一個靜態的字典變數,然後通過實現interface中宣告的set和get的實現對這個字典變數做存取操作,或者通過訊息轉發中的 (id)forwardingTargetForSelector:(SEL)aSelector 方法返回這個字典變數,但是要注意本類中沒有對轉發做過什麼事,不然這種方法也是不適用的。

對上文的總結

其實剛剛所描述的三種分類策略並不是很嚴謹,因為其中幾種總是會搭配著使用,所以在此也要選擇一個比較均衡的策略來實現Category屬性的繫結。

建議的策略:

  1. 由於我們肯定會在interface 中提供生的property(由於沒有合成實現與ivar,在此稱為生的),所以這樣對於在外部訪問時和普通property相同。
  2. 由於缺乏的是實現以及可以存取的資料量,這裡我們可以直接實現這些set與get。
  3. set與get的實現可以通過 associatedObject 進行對物件的存取操作。

好處: 這種操作由於提供了生的property,所以在第三方的json轉model庫遍歷property時可以直接遍歷到,由於你手動實現了set與get,所以在遍歷後的KVC賦值時也能起到作用,保證了和普通成員變數的操作一致性。

估計會有人看完結論後覺得:“ 我本來就是這麼寫的啊,你寫這麼多字到頭來得出的結論和我平時寫的也一樣。”是的,我只能略表抱歉啦?!

相關文章