2019年年初iOS招人心得笔记 答案 (四)

1、UITableview的优化方法(缓存高度,异步绘制,减少层级,hide,避免离屏渲染)

参考链接:
https://www.jianshu.com/p/2d077da3af94

2、有没有用过运行时,用它都能做什么?(交换方法,创建类,给新创建的类增加方法,改变isa指针)

交换方法 method swizzling AOP面向切面编程
创建类 服务器页面跳转
给新创建的类添加方法 当类调用了一个没有implementation的方法的时候 可以用在防止崩溃的地方
改变isa指针 KVO的原理

3、看过哪些第三方框架的源码?都是如何实现的?(如果没有,问一下多图下载的设计)

多图下载功能,也就是对于SDWebImage的使用和理解。

多图下载遇到的问题

1.图片加载流程:
      1. 图片存在判断:先加载image,通过image是否为空判断,不为空,返回图片;不为空,通过另外方式加载,继续判断;如果通过路径是否存在,数组、字典包含元素等方式判断比较麻烦

      2. 加载顺序:图片缓存(一级缓存)加载--磁盘缓存(二级缓存)加载--先用占位图片显示,新开队列及任务下载图片

  2.缓存处理:

      1. 图片缓存外的图片在获取时都要写入图片缓存,在主线程中立即写入。即从沙盒中找到图片还是下载完图片后都要写入图片缓存中(一级缓存)

      2. 磁盘缓存外的图片,在下载完后要写入,由于写入操作耗时,可以在子线程中执行。(二级缓存)

      3. 图片缓存和磁盘缓存建议使用字典方式保存,Key值可以用图片的后缀名保存;保存前要对value值进行非空判断

      4. 磁盘缓存地址:为了方便下次使用,最好将数据写入沙盒中,方便以后直接使用。documents下面的文件会被备份,另外苹果官方严禁将下载的图片放到documents,弃之。library--perference保存偏好设置的,弃之;tmp中的文件会被随即删除,弃之。最终方案是放在library--cache中,不会备份,定期可删除

      5. 为避免重复下载,可设置任务缓存,每次创建新任务前先判断是否已经存在任务,若存在则等待;图片下载后(无论成功与失败)都应该清空任务缓存

  3.图片下载:

      1. 在下载图片前,主线程先用占位图片显示cell.imageView.image.

      2. 下载任务可以封装成一个方法来异步执行

      3. 先根据app.icon从任务缓存中加载任务,判断任务是否已经在operations中,若是,则等待下载完毕;否则再创建新的任务

      4. 小文件的下载直接通过NSData下载最好,使用NSURLSessionDownLoadTask-block还是会有点麻烦

      5. 网络请求非空判断:图片下载完毕后,在写入图片缓存前,需要进行非空判断,这是因为字典保存的value不能为空,所以当下载的图片为空时,要先移除操作缓存,并返回。移除操作缓存是因为不移除,下次就不会重新加载)

      6. 最终下载完毕后需要实现:1.回到主线程刷新UI,并写入图片缓存;2.清除下载任务缓存;3.将图片写入到沙盒缓存

      7. 下载图片耗时,应该新开子线程来下载图片;可以通过NSOperationQueue来下载任务(懒加载非主队列),并设置最大并发数优化性能

  4. 主线程刷新UI
      1. 下载完毕后要回到主线程刷新UI。

      2. 由于cell的循环利用,所以刷新要通过reloadRowsAtIndexPaths

  5. 内存警告处理:
      1.将数据保存到字典中时,可能会收到内存警告,这时要情况所有内存图片和操作缓存,并停止队列,使程序得以保存)
###在vc.m中
@interface ViewController ()
/** tableView的数据源 */
@property (nonatomic ,strong)NSArray *apps;
/** 图片缓存*/
@property (nonatomic ,strong ,nonnull)NSMutableDictionary *images;
/** 任务缓存 */
@property (nonatomic ,strong ,nonnull)NSMutableDictionary *operations;
/** 队列 */
@property (nonatomic ,strong)NSOperationQueue *queue;
@end

@implementation ViewController

#pragma mark - lazy loading

-(NSArray *)apps
{
    if (_apps == nil) {

        //1.加载本地的plist文件
        NSArray *appplistArray = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle]pathForResource:@"apps.plist" ofType:nil]];

        //2.字典转模型 appplistArray(字典数组)---->模型数组
        NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:appplistArray.count];

        for (NSDictionary *dict in appplistArray) {
            [arrayM addObject:[FZQApp appWithDict:dict]];
        }

        _apps = arrayM;
    }
    return _apps;
}

-(NSMutableDictionary *)images
{
    if (_images == nil) {
        _images = [NSMutableDictionary dictionary];
    }
    return _images;
}

-(NSMutableDictionary *)operations
{
    if (_operations == nil) {
        _operations = [NSMutableDictionary dictionary];
    }
    return _operations;
}

-(NSOperationQueue *)queue
{
    if (_queue == nil) {
        //创建队列
        _queue = [[NSOperationQueue alloc]init];
        //设置最大并发数
        _queue.maxConcurrentOperationCount = 5;
    }
    return _queue;
}
#pragma mark ---------------------------
#pragma mark UITbaleViewDataSource

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.apps.count;
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *ID = @"app";
    //1.创建cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
    //2.设置cell的数据

    //2.1 取出该cell对应的数据
    FZQApp *app = self.apps[indexPath.row];

    //2.2 设置
    cell.textLabel.text = app.name;
    cell.detailTextLabel.text = app.download;
    cell.imageView.image = [self setUpImage:self.apps[indexPath.row] indexPath:indexPath];

    //3.返回cell
    return cell;
}


#pragma mark - Life Cycle
//收到内存警告时清除缓存和队列任务
-(void)didReceiveMemoryWarning
{
    self.images = nil;
    self.operations = nil;
    [self.queue cancelAllOperations];
}

@end


#pragma mark - Methods
/** 加载并设置图片 */
-(UIImage *)setUpImage:(FZQApp *)app indexPath:(NSIndexPath *)indexPath
{
    /********           缓存中查找图片          ********/
    //从缓存中获取图片
    UIImage *image = self.images[app.iconUrl];

    //判断图片是否已经存在,存在则直接显示
    if (image) return image;


    /********           磁盘缓存中查找图片          ********/
    //获取cache路径
    NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];

    //拼接图片磁盘缓存路径
    NSString *imgsPath = [cachePath stringByAppendingPathComponent:[app.iconUrl lastPathComponent]];

    //从磁盘中获取图片
    image = [UIImage imageWithContentsOfFile:imgsPath];

    //磁盘缓存中是否存在图片
    if(image){

            //存在写入到内存缓存中,并返回图片
            [self.images setObject:image forKey:app.iconUrl];

            return image;
    }

    /********           网络上加载图片          ********/
    //若不存在则到网上去下载图片,先用占位图片显示
    image = IMAGE(@"Snip20151102_160.png");

    //生成下载图片任务
    [self downLoadOperation:app indexPath:indexPath imagesPath:imgsPath];

    return image;
}

/* 设置下载任务 */
- (void)downLoadOperation:(FZQApp *)app indexPath:(NSIndexPath *)indexPath imagesPath:(NSString *)imagesPath
{
    //设置下载任务
    NSBlockOperation *downLoadOpe = self.operations[app.iconUrl];

    //判断下载任务是否存在
    if(downLoadOpe == nil){

        //不存在则新建下载任务,并记录下载任务
        downLoadOpe = [NSBlockOperation blockOperationWithBlock:^{

            //加载数据
            NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:app.iconUrl]];

            //下载图片
            UIImage *image = [UIImage imageWithData:data];

            //判断下载图片是否为空,若是需要清除下载任务并返回
            if (!image) {
                //清除任务
                [self.operations removeObjectForKey:app.iconUrl];

                //返回
                return ;
            }

            //回到主线程刷新UI
            [self refreshView:indexPath image:image];

            //写入内存缓存
            [self.images setObject:image forKey:app.iconUrl];

            //写入到磁盘缓存中
            [data writeToFile:imagesPath atomically:YES];

            //清除下载任务
            [self.operations removeObjectForKey:app.iconUrl];
        }];

        //添加任务
        [self.queue addOperation:downLoadOpe];

        // 记录下载任务
        [self.operations setObject:downLoadOpe forKey:app.iconUrl];
    }
}

/* 刷新UI */
- (void)refreshView:(NSIndexPath *)indexPath image:(UIImage *)image
{
    //下载完毕后回到主线程刷新数据
    [[NSOperationQueue mainQueue]addOperationWithBlock:^{

        //设置cell图片
        [self.tableView cellForRowAtIndexPath:indexPath].imageView.image = image;

        //刷新数据
        [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    }];
}


###在模型.h中
@interface FZQApp : NSObject

/** 名称 */
@property (nonatomic ,strong)NSString *name;
/** 图片的url */
@property (nonatomic ,strong)NSString *icon;
/** 下载数量 */
@property (nonatomic ,strong)NSString *download;

+(instancetype)appWithDict:(NSDictionary *)dict;
@end

###在模型.m中
@implementation FZQApp

+(instancetype)appWithDict:(NSDictionary *)dict
{
    FZQApp *app = [[FZQApp alloc]init];
    //KVC
    [app setValuesForKeysWithDictionary:dict];

    return app;
}
@end

参考链接:
https://www.jianshu.com/p/276d0c6a34cc

4、SDWebImage的缓存策略?

首先看一下SDWebImage的整体结构:


image.png

有一个专门的Cache分类用来处理图片的缓存。这里面有两个类SDImageCache和SDImageCacheConfig。大部分的缓存处理都在SDImageCache这个类实现。

Memory和Disk双缓存

首先介绍一下 Memory Cache,贴一段 SDImageCache 的代码:

@interface SDImageCache ()

#pragma mark - Properties
@property (strong, nonatomic, nonnull) NSCache *memCache;

...

这里我们发现,又一个叫做 memCache 的属性,它是一个 NSCache 对象,用于实现我们对图片的 Memory Cache。SDWebImage 还专门实现了一个叫做 AutoPurgeCache 的类,相比于简单的 NSCache,它提供了一个在内存紧张时候释放缓存的能力:

@interface AutoPurgeCache : NSCache
@end

@implementation AutoPurgeCache

- (nonnull instancetype)init {
    self = [super init];
    if (self) {
#if SD_UIKIT
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
    }
    return self;
}

其实就是接受系统的内存警告通知,然后清除掉自身的图片缓存。这里大家比较少见的一个类应该是 NSCache 了。简单来说,它是一个类似于 NSDictionary 的集合类,用于在内存中存储我们要缓存的数据。

说完 Menory Cache,我们再来说说 DIsk Cache,也就是文件缓存。

SDWebImage 会将图片存放到 NSCachesDirectory 目录中:

- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace {
    NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    return [paths[0] stringByAppendingPathComponent:fullNamespace];
}

然后为每一个缓存文件生成一个md5文件夹,存放到文件中。

整体机制

下面我们看看当使用 SDImageCache 读取图片时候的完整流程。我们一般会使用 SDWebImage 对 UIKit 的扩展,直接加载图片:

[imageView sd_setImageWithURL:[NSURL URLWithString:@"url"]];

首先这个 Category 方法 sd_setImageWithURL 内部会调用 SDWebImageManager 的 downloadImageWithURL 方法来处理这个图片URL:

id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            ...
}];

SDWebImageManager 内部的 downloadImageWithURL 方法会先使用我们前面提到的 SDImageCache 类的 queryDiskCacheForKey 方法,查找图片缓存:

operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {

    ...
}];

再来看 queryDiskCacheForKey 方法内部,先会查询 Memory Cache:

UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
    doneBlock(image, SDImageCacheTypeMemory);
    return nil;
}

如果 Memory Cache 查找不到了,就会查询 Disk Cache:

dispatch_async(self.ioQueue, ^{
    if (operation.isCancelled) {
        return;
    }

    @autoreleasepool {
        UIImage *diskImage = [self diskImageForKey:key];
        if (diskImage && self.shouldCacheImagesInMemory) {
            NSUInteger cost = SDCacheCostForImage(diskImage);
            [self.memCache setObject:diskImage forKey:key cost:cost];
        }

        dispatch_async(dispatch_get_main_queue(), ^{
            doneBlock(diskImage, SDImageCacheTypeDisk);
        });
    }
});

查询 Disk Cache 的时候有一个小插曲,就是如果 Disk Cache 查询成功,还会把得到的图片再一次设置到 Memory Cache 中。这样做可以最大化提高查找高频使用的图片的效率。

如果缓存查找成功,那么就会直接返回缓存的数据。如果不成功,接下来就开始请求网络:

id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
}

请求网络使用的是 imageDownloader 属性,这个实例专门负责下载图片资源,如果下载失败,会把失败的图片地址写入 failedURLs 集合:

if (   error.code != NSURLErrorNotConnectedToInternet
    && error.code != NSURLErrorCancelled
    && error.code != NSURLErrorTimedOut
    && error.code != NSURLErrorInternationalRoamingOff
    && error.code != NSURLErrorDataNotAllowed
    && error.code != NSURLErrorCannotFindHost
    && error.code != NSURLErrorCannotConnectToHost) {
    @synchronized (self.failedURLs) {
        [self.failedURLs addObject:url];
    }
}

SDWebImage 默认会有一个对上次加载失败的图片拒绝再次加载的机制。也就是说,一张图片在本次会话加载失败了,如果再次加载就会直接拒绝。SDWebImage 这样做可能是为了提高性能吧。这个机制可能会被大家忽略。

如果图片下载成功了,接下来会使用 [self.imageCache storeImage] 方法将它写入缓存,并且调用 completedBlock 告诉前端现实图片:

if (downloadedImage && finished) {
    [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}

dispatch_main_sync_safe(^{
    if (strongOperation && !strongOperation.isCancelled) {
        completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
    }
});
是否重试失败的 URL

你可以在加载图片的时候设置 SDWebImageRetryFailed 标记,这样 SDWebImage 就会加载之前失败过的图片了。记得我们之前提到过 failedUURLs 属性了吧,这个属性是在内存中存储的,如果图片加载失败,SDWebImage 会在本次 App 会话中都不再重试这张图片了。当然这个加载失败是有条件的,如果是超时失败,不记在内。

如果你需要图片的可用性,而不是这一点点的性能优化,那么你就可以带上 SDWebImageRetryFailed 标记:

[_image sd_setImageWithURL:[NSURL URLWithString:@"url"] placeholderImage:nil options:SDWebImageRetryFailed];
Disk 缓存清理策略

SDWebImage 会在每次 App 借宿的时候执行清理。清理缓存的规则分两步进行。第一步先清理掉过期的存储文件。如果清理掉过期的缓存之后,空间还是不够。那么就继续按文件时间从早到晚排序,先清理最早的缓存文件,知道剩余空间达到要求。

具体点,SDWebImage是怎么控制哪些缓存过期,以及剩余空间多少才够呢?通过两个属性:

@interface SDImageCache : NSObject

@property (assign, nonatomic) NSInteger maxCacheAge;
@property (assign, nonatomic) NSUInteger maxCacheSize;

maxCacheAge 是文件缓存的时长,SDWebImage 会注册两个通知:

[[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(cleanDisk)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(backgroundCleanDisk)
                                             name:UIApplicationDidEnterBackgroundNotification
                                           object:nil];

分别在应用进入后台和结束的时候,遍历所有的缓存文件,如果缓存文件超过 maxCacheAge 中指定的时长,就会被删除掉。

同样的,maxCacheSize 控制 SDImageCache 所允许的最大缓存空间。如果清理完过期文件后缓存空间依然没达到 maxCacheSize 的要求,那么就会继续清理旧文件,直到缓存空间达到要求为止。

maxCacheAge 和 maxCacheSize 都是有默认值的:

static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week

maxCacheSize 默认情况下不会对缓存空间设限制。

if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
    //清理缓存代码
}

说明一下,上面代码的 currentCacheSize 变量代表当前图片缓存占用的空间。从这里可以看出,只有在 maxCacheSize 大于 0 并且当前缓存空间大于 maxCacheSize 的时候才进行第二步的缓存清理。

参考链接:
http://swiftcafe.io/2017/02/19/sdimage-cache/

5、AFN为什么添加一条常驻线程?

AFN 的做法是把网络请求和解析都放在同一个子线程中进行,但由于子线程默认不开启runloop,它会向一个C语言程序一样在运行完所有代码后退出线程。而网络请求是异步的,这会导致获取到请求数据时,线程已经退出了,代理方法没有机会执行。因此,AFN的做法是使用一个runloop来保证线程不死。

然而频繁的创建线程并启动runloop肯定会造成内存泄漏(runloop 无法停止,线程无法退出)

所以 AFN 就创建了一个单例线程,并且保证线程不退出。

6、KVO的使用?实现原理?(为什么要创建子类来实现)

KVO<NSKeyValueObserving>,是一个非正式协议,提供了一个途径,使对象(观察者)能够观察其他对象(被观察者)的属性,当被观察者的属性发生改变时,观察者就会被告知该变化。

基本使用
//添加观察者
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

//实现观察者响应方法
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary*)change context:(nullable void *)context;

//移除观察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
实现原理

KVO 的实现也依赖于 OC 强大的 runtime 。Apple 的文档有简单提到过 KVO 的实现:

Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...

被观察对象的 isa 指针会指向一个中间类,而不是原来真正的类。

当你观察一个对象时,一个新的类会被动态创建(NSKVONotifying_Obj)。这个类继承自该对象的本来的类,重写了被观察属性的 setter 方法,添加willChangeValueForKey : didChangevlueForKey :,并调用observeValueForKey:ofObject:change:context:方法通知被观察者。最后把被观察者对象的isa指针指向 NSKVONotifying_Obj ,这样就能够做到观察属性的功能。

参考链接:
https://xiaozhuanlan.com/topic/0892715634

7、KVC的使用?实现原理?(KVC拿到key以后,是如何赋值的?知不知道集合操作符,能不能访问私有属性,能不能直接访问_ivar)

KVC
key value coding
键值编码
可以通过key来访问属性

常见的API有:

- (void)setValue:(id)value forKey:(NSString *)key;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;

- (id)valueForKey:(NSString *)key; 
- (id)valueForKeyPath:(NSString *)keyPath;
setValue:forKey:的原理
image.png

当我们使用setValue:forKey:时,首先会查找setKey:、_setKey: (按顺序查找),如果有直接调用,如果没有,先查看accessInstanceVariablesDirectly方法。

+ (BOOL)accessInstanceVariablesDirectly{
      return YES;   ///> 可以直接访问成员变量
  //    return NO;  ///>  不可以直接访问成员变量,  
  ///> 直接访问会报NSUnkonwKeyException错误  
  }

如果可以访问会按照 _key、_isKey、key、iskey的顺序查找成员变量,找到后直接赋值,未找到报NSUnkonwKeyException。

valueForKey:的原理
image.png

kvc取值按照 getKey、key、iskey、_key 顺序查找方法,存在直接调用。没有的话,先查看accessInstanceVariablesDirectly方法。如果可以访问会按照 _key、_isKey、key、iskey的顺序查找成员变量,找到直接返回,未找到报NSUnkonwKeyException。

集合操作符:

KVC集合操作符的类型

  • Simple Collection Operators 简单的集合操作符
  • Object Operators 对象操作符
  • Array and Set Operators 数组/集合操作符
Simple Collection Operators
  • @count
  • @avg
  • @max
  • @min
  • @sum

Notice: 所有的集合操作,除了@count外,其他都需要有右边的keyPath(一般为属性名),@count右边的keyPath可写可不写.

//price是product中的一个属性
NSArray *product = @[productA, productB, productC, productD];
NSNumber *count = [product valueForKeyPath:@"@count.price"];
NSNumber *avg = [product valueForKeyPath:@"@avg.price"];
NSNumber *max = [product valueForKeyPath:@"@max.price"];
NSNumber *min = [product valueForKeyPath:@"@min.price"];
NSNumber *sum = [product valueForKeyPath:@"@sum.price"];
NSLog(@"count:%@, avg:%@, max:%@, min:%@, sum:%@", count, avg, max, min, sum); // count:4, avg:199, max:299, min:99, sum:796
Object Operators
  • @unionOfObjects: 获取数组中每个对象的属性的值,放到一个数组中并返回,但不会去重;The @unionOfObjects operator provides similar behavior, but without removing duplicate objects.
  • @distinctUnionOfObjects:获取数组中每个对象的属性的值,放到一个数组中并返回,会对数组去重.所以,通常这个对象操作符可以用来对数组元素的去重,快捷高效;The @distinctUnionOfArrays operator is similar, but removes duplicate objects.
NSArray *unionOfObjects = [product valueForKeyPath:@"@unionOfObjects.name"];
NSArray *distinctUnionOfObjects = [product valueForKeyPath:@"@distinctUnionOfObjects.name"];
NSLog(@"unionOfObjects : %@", unionOfObjects);//iPod,iMac,iPhone,iPhone
NSLog(@"distinctUnionOfObjects : %@", distinctUnionOfObjects);//iPhone,iPod,iMac
Array and Set Operators
  • @distinctUnionOfArrays 返回操作对象(数组)中的所有元素,即返回这个数组本身.会去重.
  • @unionOfArrays 首先获取操作对象(数组)中的所有元素,然后装到一个新的数组中并返回,不会对这个数组去重.
NSArray *distinctUnionOfArrays = [@[product, product] valueForKeyPath:@"@distinctUnionOfArrays.price"];
NSArray *unionOfArrays = [@[product, product] valueForKeyPath:@"@unionOfArrays.price"];
NSLog(@"distinctUnionOfArrays : %@", distinctUnionOfArrays);//299,99,199
NSLog(@"unionOfArrays : %@", unionOfArrays);//99,199,299,199,99,199,299,199
  • @distinctUnionOfSets返回操作对象(且操作对象内对象必须是数组/集合)中数组/集合的所有对象,返回值为集合.因为集合不能包含重复的值,所以它只有distinct操作

可以访问_ivar

@interface NSObject(MYKVC)
-(void)setMyValue:(id)value forKey:(NSString*)key;
-(id)myValueforKey:(NSString*)key;

@end
@implementation NSObject(MYKVC)
-(void)setMyValue:(id)value forKey:(NSString *)key{
    if (key == nil || key.length == 0) {  //key名要合法
        return;
    }
    if ([value isKindOfClass:[NSNull class]]) {
        [self setNilValueForKey:key]; //如果需要完全自定义,那么这里需要写一个setMyNilValueForKey,但是必要性不是很大,就省略了
        return;
    }
    if (![value isKindOfClass:[NSObject class]]) {
        @throw @"must be s NSObject type";
        return;
    }

    NSString* funcName = [NSString stringWithFormat:@"set%@:",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(funcName)]) {  //默认优先调用set方法
        [self performSelector:NSSelectorFromString(funcName) withObject:value];
        return;
    }
    unsigned int count;
    BOOL flag = false;
    Ivar* vars = class_copyIvarList([self class], &count);
    for (NSInteger i = 0; i<count; i++) {
        Ivar var = vars[i];
        NSString* keyName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:1];
        
        if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
            flag = true;
            object_setIvar(self, var, value);
            break;
        }
        
        
        if ([keyName isEqualToString:key]) {
            flag = true;
            object_setIvar(self, var, value);
            break;
        }
    }
    if (!flag) {
        [self setValue:value forUndefinedKey:key];//如果需要完全自定义,那么这里需要写一个self setMyValue:value forUndefinedKey:key,但是必要性不是很大,就省略了
    }
}

-(id)myValueforKey:(NSString *)key{
    if (key == nil || key.length == 0) {
        return [NSNull new]; //其实不能这么写的
    }
    //这里为了更方便,我就不做相关集合的方法查询了
    NSString* funcName = [NSString stringWithFormat:@"gett%@:",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(funcName)]) {
       return [self performSelector:NSSelectorFromString(funcName)];
    }

    unsigned int count;
    BOOL flag = false;
    Ivar* vars = class_copyIvarList([self class], &count);
    for (NSInteger i = 0; i<count; i++) {
        Ivar var = vars[i];
        NSString* keyName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:1];
        if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
            flag = true;
            return     object_getIvar(self, var);
            break;
        }
        if ([keyName isEqualToString:key]) {
            flag = true;
            return     object_getIvar(self, var);
            break;
        }
    }
    if (!flag) {
        [self valueForUndefinedKey:key];//如果需要完全自定义,那么这里需要写一个self myValueForUndefinedKey,但是必要性不是很大,就省略了
    }
   return [NSNull new]; //其实不能这么写的
}
@end


Address* add = [Address new];
add.country = @"China";
add.province = @"Guang Dong";
add.city = @"Shen Zhen";
add.district = @"Nan Shan";

[add setMyValue:nil forKey:@"area"];            //测试设置 nil value
[add setMyValue:@"UK" forKey:@"country"];
[add setMyValue:@"South" forKey:@"area"];
[add setMyValue:@"300169" forKey:@"postCode"];
NSLog(@"country:%@  province:%@ city:%@ postCode:%@",add.country,add.province,add.city,add._postCode);
NSString* postCode = [add myValueforKey:@"postCode"];
NSString* country = [add myValueforKey:@"country"];
NSLog(@"country:%@ postCode: %@",country,postCode);

//打印结果:

2016-04-19 14:29:39.498 KVCDemo[7273:275129] country:UK  province:South city:Shen Zhen postCode:300169
2016-04-19 14:29:39.499 KVCDemo[7273:275129] country:UK postCode: 300169

参考链接:
https://www.jianshu.com/p/2c2af5695904
https://www.jianshu.com/p/45cbd324ea65

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