GVUserDefaults原始碼閱讀及使用
最近比較清閒,就把以前學習的過程記錄下吧,多少年後如果能在網際網路上找到自己的痕跡,想想還是一件蠻值得高興的事情
涉及到的知識點
@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值。
當然你可以只建立一個.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 裡面分別打上斷點,並且執行
發現它並沒有走到if 裡面去 ,而且getter為null 根據我們對@property 所知道的,編譯器會幫助我們自動生成setter方法 和 getter 方法啊,按理說getter應該是有值的啊。再接著往下看,如果我在宣告屬性的時候這樣來寫
然後再執行來看
發現它就走到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方法。
學無止境啊,讓我們一起努力吧。。。。
相關文章
- 【原始碼閱讀】AndPermission原始碼閱讀原始碼
- 使用OpenGrok閱讀原始碼原始碼
- 使用vim閱讀原始碼原始碼
- 【原始碼閱讀】Glide原始碼閱讀之with方法(一)原始碼IDE
- 【原始碼閱讀】Glide原始碼閱讀之into方法(三)原始碼IDE
- ReactorKit原始碼閱讀React原始碼
- AQS原始碼閱讀AQS原始碼
- CountDownLatch原始碼閱讀CountDownLatch原始碼
- HashMap 原始碼閱讀HashMap原始碼
- delta原始碼閱讀原始碼
- 原始碼閱讀-HashMap原始碼HashMap
- NGINX原始碼閱讀Nginx原始碼
- Mux 原始碼閱讀UX原始碼
- HashMap原始碼閱讀HashMap原始碼
- fuzz原始碼閱讀原始碼
- RunLoop 原始碼閱讀OOP原始碼
- express 原始碼閱讀Express原始碼
- muduo原始碼閱讀原始碼
- stack原始碼閱讀原始碼
- 原始碼閱讀:Masonry(一)——從使用入手原始碼
- 原始碼閱讀:SDWebImage(一)——從使用入手原始碼Web
- 【原始碼閱讀】Glide原始碼閱讀之load方法(二)原始碼IDE
- PostgreSQL 原始碼解讀(3)- 如何閱讀原始碼SQL原始碼
- 使用 SourceInsight 閱讀、編輯原始碼原始碼
- 搭建大型原始碼閱讀環境——使用 OpenGrok原始碼
- JDK原始碼閱讀:Object類閱讀筆記JDK原始碼Object筆記
- Laravel 原始碼閱讀 - QueueLaravel原始碼
- Vollery原始碼閱讀(—)原始碼
- 如何閱讀Java原始碼?Java原始碼
- buffer 原始碼包閱讀原始碼
- 原始碼閱讀技巧篇原始碼
- 如何閱讀框架原始碼框架原始碼
- 再談原始碼閱讀原始碼
- Laravel 原始碼閱讀 - EloquentLaravel原始碼
- 如何閱讀jdk原始碼?JDK原始碼
- express 原始碼閱讀(全)Express原始碼
- Vuex原始碼閱讀分析Vue原始碼
- React原始碼閱讀:setStateReact原始碼