GVUserDefaults原始碼閱讀及使用

weixin_33785972發表於2017-06-15

最近比較清閒,就把以前學習的過程記錄下吧,多少年後如果能在網際網路上找到自己的痕跡,想想還是一件蠻值得高興的事情

涉及到的知識點

@dynamic
objc/runtime.h

@dynamic

說起dynamic 就要提跟它相關的@property 和 @synthesize
@property 是iOS 6以後出現的,使用它生成的變數,編譯器會自動幫我們生成setter 和 getter方法,還有一個系統預設生成的以 _開頭的變數,它相當於在.m中幫我們自動@synthesize varName = _varName 。@dynamic 就是告訴編譯器,這兩個方法我自己去實現,讓編譯器去先通過編譯,但是如果執行的時候不去動態繫結它的setter 和getter方法,而去呼叫它的話,程式就會崩潰.

NSUserDefaults

我相信專案中大家都有用到NSUserDefaults ,而且非常普遍,用起來也順手。最常用的就是我們用它來儲存使用者的登入狀態,然後一些登入之後才能做的操作,就從本地去取這個狀態的值,從而去判斷使用者是否登入

比方說有一個需求是儲存使用者的手機號碼,下次進入登入頁面的時候,免除使用者填寫手機號碼的操作

 [[NSUserDefaults standardUserDefaults] setObject:@"18888888888" forKey:@"phoneAccount"];
    [[NSUserDefaults standardUserDefaults] synchronize];

其實程式碼很簡單,但是每一次都要去寫兩行程式碼才能實現這個操作。當然我們可以把它寫成巨集,比方說這樣

#define SET_OBJECT(object, key)                                                                                                 \
({                                                                                                                                             \
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];                                                                          \
[defaults setObject:object forKey:key];                                                                                                    \
[defaults synchronize];                                                                                                                    \
})

這樣的話,又簡單了一些。但是往往我們的專案裡面並不只是登入這塊需要用到NSUserDefaults ,需要記錄的資料不止這些。那麼我們想把程式碼寫的更規範些,更清晰,更具可讀性的話,我們最好還是建立一個.h和.m檔案專門記錄我們所需要儲存資料的key值。

2916672-c1d69098a3557a5d.png
3CEDC9ED-3BA0-43E2-9E2C-26B9783C063A.png
2916672-0254322157017e56.png
3BAEFFF0-4D1E-4DD5-812E-50F398DE94BD.png

當然你可以只建立一個.h檔案 使用巨集去記錄,但是蘋果推薦我們使用這種方式去記錄一個常量
有了上面這些操作是不是就變的很方便了,確實方便。但是有一個第三方的輪子,能讓我們更方便的去替代上面這些操作

GVUserDefaults GitHub

到現在為止已經800多star了,還是蠻好用的。直接看原始碼,它的原始碼只有一個.h 和一個.m檔案。.h中只有一個建立單例的類方法

+ (instancetype)standardUserDefaults;

我們一步一步看,點進去看它的init方法

- (instancetype)init {
    self = [super init];
    if (self) {
        // 獲取一個SEL 型別的 選擇器
        SEL setupDefaultSEL = NSSelectorFromString([NSString stringWithFormat:@"%@pDefaults", @"setu"]);
        // 這個if 裡面的操作後面會說到
        if ([self respondsToSelector:setupDefaultSEL]) {
            NSDictionary *defaults = [self performSelector:setupDefaultSEL];
            NSMutableDictionary *mutableDefaults = [NSMutableDictionary dictionaryWithCapacity:[defaults count]];
            for (NSString *key in defaults) {
                id value = [defaults objectForKey:key];
                NSString *transformedKey = [self _transformKey:key];
                [mutableDefaults setObject:value forKey:transformedKey];
            }
            // 相當於把mutableDefaults 裡面的資料 配置為本地的預設值
            [self.userDefaults registerDefaults:mutableDefaults];
        }
        [self generateAccessorMethods];
    }

    return self;
}

我們再點進去 方法 generateAccessorMethods ,核心程式碼也都在這裡


- (void)generateAccessorMethods {
    unsigned int count = 0;
    objc_property_t *properties = class_copyPropertyList([self class], &count);

    self.mapping = [NSMutableDictionary dictionary];

    for (int i = 0; i < count; ++i) {
        objc_property_t property = properties[i];
        const char *name = property_getName(property);
        const char *attributes = property_getAttributes(property);

        char *getter = strstr(attributes, ",G");
        if (getter) {
            getter = strdup(getter + 2);
            getter = strsep(&getter, ",");
        } else {
            getter = strdup(name);
        }
        SEL getterSel = sel_registerName(getter);
        free(getter);

        char *setter = strstr(attributes, ",S");
        if (setter) {
            setter = strdup(setter + 2);
            setter = strsep(&setter, ",");
        } else {
            asprintf(&setter, "set%c%s:", toupper(name[0]), name + 1);
        }
        SEL setterSel = sel_registerName(setter);
        free(setter);

        NSString *key = [self defaultsKeyForPropertyNamed:name];
        [self.mapping setValue:key forKey:NSStringFromSelector(getterSel)];
        [self.mapping setValue:key forKey:NSStringFromSelector(setterSel)];

        IMP getterImp = NULL;
        IMP setterImp = NULL;
        char type = attributes[1];
        switch (type) {
            case Short:
            case Long:
            case LongLong:
            case UnsignedChar:
            case UnsignedShort:
            case UnsignedInt:
            case UnsignedLong:
            case UnsignedLongLong:
                getterImp = (IMP)longLongGetter;
                setterImp = (IMP)longLongSetter;
                break;

            case Bool:
            case Char:
                getterImp = (IMP)boolGetter;
                setterImp = (IMP)boolSetter;
                break;

            case Int:
                getterImp = (IMP)integerGetter;
                setterImp = (IMP)integerSetter;
                break;

            case Float:
                getterImp = (IMP)floatGetter;
                setterImp = (IMP)floatSetter;
                break;

            case Double:
                getterImp = (IMP)doubleGetter;
                setterImp = (IMP)doubleSetter;
                break;

            case Object:
                getterImp = (IMP)objectGetter;
                setterImp = (IMP)objectSetter;
                break;

            default:
                free(properties);
                [NSException raise:NSInternalInconsistencyException format:@"Unsupported type of property \"%s\" in class %@", name, self];
                break;
        }

        char types[5];

        snprintf(types, 4, "%c@:", type);
        class_addMethod([self class], getterSel, getterImp, types);
        
        snprintf(types, 5, "v@:%c", type);
        class_addMethod([self class], setterSel, setterImp, types);
    }

    free(properties);
}

我們都知道OC是一門動態語言,嚴重依賴於runtime庫。
類Class 在runtime庫中由結構體組成,它在runtime中的結構體我們也不會陌生.

typedef struct objc_class *Class;

/*
  這是由編譯器為每個類產生的資料結構,這個結構定義了一個類.這個結構是通過編譯器在執行時產生,在執行時傳送訊息時使用.因此,一些成員改變了型別.編譯器產生"char* const"型別的字串指標替代了下面的成員變數"super_class"
*/
struct objc_class {
  struct objc_class*  class_pointer;    /* 指向元類的指標. */
  struct objc_class*  super_class;      /* 指向父類的指標. 對於NSObject來說是NULL.*/
  const char*         name;             /* 類的名稱. */
  long                version;          /* 未知. */
  unsigned long       info;             /* 位元蒙板.  參考下面類的蒙板定義. */
  long                instance_size;    /* 類的位元組數.包含類的定義和所有父類的定義 */
#ifdef _WIN64
  long pad;
#endif
  struct objc_ivar_list* ivars;         /* 指向類中定義的例項變數的列表結構. NULL代表沒有例項變數.不包括父類的變數. */
  struct objc_method_list*  methods;    /* 連結類中定義的例項方法. */
  struct sarray *    dtable;            /* 指向例項方法分配表. */
  struct objc_class* subclass_list;     /* 父類列表 */
  struct objc_class* sibling_class;
  struct objc_protocol_list *protocols; /* 要實現的原型列表 */
  void* gc_object_type;
};

先看第一個方法

objc_property_t *properties = class_copyPropertyList([self class], &count);

獲取當前Class的所有屬性
然後for迴圈 獲取每一個屬性的name 和 屬性特性描述字串

     const char *attributes = property_getAttributes(property);

我們也可以利用函式

 unsigned int attrCount = 0;
        objc_property_attribute_t * attrs = property_copyAttributeList(property, &attrCount);
        for (unsigned int j = 0; j < attrCount; j ++) {
            objc_property_attribute_t attr = attrs[j];
            const char * name = attr.name;
            const char * value = attr.value;
        }
        free(attrs);

獲取變數的屬性的特性,objc_property_attribute_t結構體包含name和value 常用的屬性:

屬性型別  name值:T  value:變化
編碼型別  name值:C(copy) &(strong) W(weak) 空(assign) G(getter=method)S(setter=method)D(dynamic)等 value:無
非/原子性 name值:空(atomic) N(Nonatomic)  value:無
變數名稱  name值:V  value:變化

比方說有一個屬性 @property (nonatomic, copy) NSString *teacher;
那麼使用property_copyAttributeList 獲取到的objc_property_attribute_t結構體列表中的描述就是 T@"NSString",C,N,V_teacher

接著往下看,原始碼中這一段

   char *getter = strstr(attributes, ",G");
        if (getter) {
            getter = strdup(getter + 2);
            getter = strsep(&getter, ",");
        } else {
            getter = strdup(name);
        }
        SEL getterSel = sel_registerName(getter);
        free(getter);

char *getter = strstr(attributes, ",G"); 這個方法是什麼意思呢?
從程式碼上來看 應該是用來判斷它的getter方法存不存在
我們可以建立一個測試demo來測試一下,還是teacher來舉例

我在if 和 else 裡面分別打上斷點,並且執行

2916672-971173a564a6717f.png
55AE9275-97C2-4140-96EA-1AE0ADD78B92.png

2916672-a999143e6f7b4abd.png
![Uploading 55AE9275-97C2-4140-96EA-1AE0ADD78B92_103211.png . . .]

發現它並沒有走到if 裡面去 ,而且getter為null 根據我們對@property 所知道的,編譯器會幫助我們自動生成setter方法 和 getter 方法啊,按理說getter應該是有值的啊。再接著往下看,如果我在宣告屬性的時候這樣來寫


2916672-e5a7aee86f3de7f4.png
6E1752C7-483A-4FD4-9FCB-E3410BF5E3CB.png

然後再執行來看

2916672-58ed2e34b1d23517.png
12286FF4-3DAA-47AA-BAEC-32620E96DC84.png

發現它就走到if 裡面去了,由此來看 char *getter = strstr(propertyAttr, ",G"); 這個函式就用來判斷通過描述字串獲取的objc_property_attribute_t()結構體裡面有沒有"G"的屬性,如果有就說明指定了getter方法,就由此描述字串生成getter的SEL 如果沒有就用變數名稱生成

同樣的setter方法,跟getter方法差不多。緊接著這兩句方法
NSString *key = [self defaultsKeyForPropertyNamed:name];
[self.mapping setValue:key forKey:NSStringFromSelector(getterSel)];
[self.mapping setValue:key forKey:NSStringFromSelector(setterSel)];我們點進去看下

- (NSString *)defaultsKeyForPropertyNamed:(char const *)propertyName {
    NSString *key = [NSString stringWithFormat:@"%s", propertyName];
    return [self _transformKey:key];
}
- (NSString *)_transformKey:(NSString *)key {
    if ([self respondsToSelector:@selector(transformKey:)]) {
        return [self performSelector:@selector(transformKey:) withObject:key];
    }

    return key;
}

以變數名為key值 ,SEL 對應的字串為value 儲存在mapping 裡面。

下面的

  IMP getterImp = NULL;
        IMP setterImp = NULL;
        char type = attributes[1];
        switch (type) {
            case Short:
            case Long:
            case LongLong:
            case UnsignedChar:
            case UnsignedShort:
            case UnsignedInt:
            case UnsignedLong:
            case UnsignedLongLong:
                getterImp = (IMP)longLongGetter;
                setterImp = (IMP)longLongSetter;
                break;

這裡的程式碼都是取描述字串裡第二個位元組字串去判斷變數所屬型別,分別生成setter和getter的IMP地址.

static long long longLongGetter(GVUserDefaults *self, SEL _cmd) {
    NSString *key = [self defaultsKeyForSelector:_cmd];
    return [[self.userDefaults objectForKey:key] longLongValue];
}

static void longLongSetter(GVUserDefaults *self, SEL _cmd, long long value) {
    NSString *key = [self defaultsKeyForSelector:_cmd];
    NSNumber *object = [NSNumber numberWithLongLong:value];
    [self.userDefaults setObject:object forKey:key];
}

最後就是動態新增setter方法 和 getter方法

  class_addMethod([self class], getterSel, getterImp, types);
  class_addMethod([self class], setterSel, setterImp, types);

我是這樣使用他們
建立GVUserDefaults 的分類,.h檔案

#define UserInfo [GVUserDefaults standardUserDefaults]

@property(nonatomic,weak)NSString* nickName;
@property(nonatomic,weak)NSString* account;
@property(nonatomic,weak)NSString* password;
// 恢復到初始狀態
- (void) restore;

.m檔案

@dynamic nickName;
@dynamic password;
@dynamic account;
- (void)restore{
    [self setValuesForKeysWithDictionary:[self setupDefaults]];
}
// 這個方法就是在init方法裡面if 裡面設定預設資料的方法
- (NSDictionary *)setupDefaults {
     static NSDictionary* dic = nil;
    if (dic) {
        return dic;
    }else{
        dic = @{
                @"nickName":@"",
                @"password":@"",
                @"account":@"",
                };
        return dic;
    }
}

這樣的話如果我們想使用UserDefaults 儲存一個值,就變的非常簡單,只需要在需要儲存的地方寫上 UserInfo.變數名 = 想要儲存的值就可以了。
當然github上GVUserDefaults 還提供了更改key值的方法,這裡我就不寫了,大家可以自己學習下。

總的來說,就是利用runtime庫去動態新增setter和getter方法。runtime其實在我們的平常工作中用處非常多,動態新增方法,交換方法,包括kvo的實現原理也是去動態監聽setter和getter方法。
學無止境啊,讓我們一起努力吧。。。。

相關文章