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值。

3CEDC9ED-3BA0-43E2-9E2C-26B9783C063A.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 里面分别打上断点,并且运行

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

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

发现它并没有走到if 里面去 ,而且getter为null 根据我们对@property 所知道的,编译器会帮助我们自动生成setter方法 和 getter 方法啊,按理说getter应该是有值的啊。再接着往下看,如果我在声明属性的时候这样来写


6E1752C7-483A-4FD4-9FCB-E3410BF5E3CB.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方法。
学无止境啊,让我们一起努力吧。。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 本文将讨论Java设计模式中比较重要的模式之一:装饰者模式 该系列其他文章: 安卓设计模式(一)面向对象六大设计原...
    uncochen阅读 1,595评论 1 9
  • 01 中午吃完午饭躺在床上刷朋友圈,刷到了《被马东转发点赞,阅读过百万,这个92年妹子的笔记究竟有什么魔力》这篇文...
    蛋炒饭要加肉阅读 372评论 6 1
  • 时间管理第13讲,主要是对1-12讲的一个总结和归纳。我们每天都会接收大量的资讯,但是太多的资讯犹如太少的...
    迭代凤儿阅读 425评论 0 0
  • 以前很喜欢看一些名人的演讲稿,其实有些时候看的多了反而不记得都看了什么,权当培养一些情操。但是我还是记住了...
    青草地与港湾阅读 178评论 1 0