iOS 底层以及数据问题深入研究(1)

96
李周
2017.08.26 18:19* 字数 1746

几周前有人问了我几个问题,我觉得自己能回答出来,但是深入的时候才发现自己还是浮在水表明,没有真正的去理解。所以将理解后易忽略的问题总结并记录下来


1 RunLoop --关于NStimer添加到NSRunLoopCommonModes的原因
2 Category --关于Category中无法创建实例变量的原因
3 CoreData -- 关于数据迁移以及预加载
4 总结

1 RunLoop

深入理解RunLoop
网上有很多关于RunLoop的文章,而这一篇是真正做到由浅入深的探讨。我不只一次看过这篇文章,但是和很多人一样,没有重点的看反而会忽略一些问题。所以下面我将以问答的形式将我从这篇文章中理解的知识点列举出来:

①NSTimer在添加到具有滚动效果的UIScrollView的页面时,如果解决滚动的时候NSTime不停止计时?

这个问题应该很简单,因为一个线程内一次只能存在一种mode。主线程的runLoop中有两个预置的mode:
kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。

kCFRunLoopDefaultMode是应用平时所处的状态。NSTimer默认加入kCFRunLoopDefaultMode中。
UITrackingRunLoopMode 是追踪 ScrollView 滑动时的状态,所以当滚动的时候会切换到该mode中。

所以当切换到UITrackingRunLoopMode中时,kCFRunLoopDefaultMode就无法同步到相应的变化了。这就解释了为什么NSTimer会在滚动的时候停止,通用的处理方法是:

 NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(updateCount) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

将NSTimer添加到NSRunLoopCommonModes中,这样就能在滚动的时候收到回调了。但是这时候会出现第二个问题:

① NSRunLoopCommonModes是mode吗?线程一次只能存在一种mode,为什么UITrackingRunLoopMode和kCFRunLoopDefaultMode都能收到事件回调?

首先,来看看NSRunLoopCommonModes的定义:

FOUNDATION_EXPORT NSRunLoopMode const NSRunLoopCommonModes API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

NSRunLoopCommonModes是NSRunLoopModel的一个常量.

typedef NSString * NSRunLoopMode NS_EXTENSIBLE_STRING_ENUM;

而NSRunLoopModel只是一个字符串而已。
所以明确的第一点是:使用的NSRunLoopCommonModes只是一个字符串标识符,并不是一个mode。
其次,来看看CFRunLoop和CFRunLoopMode结构体:

struct __CFRunLoopMode {
    CFStringRef _name;           
    CFMutableSetRef _sources0;   
    CFMutableSetRef _sources1;   
    CFMutableArrayRef _observers; 
    CFMutableArrayRef _timers; 
    ...
};
struct __CFRunLoop {
    CFMutableSetRef _commonModes; 
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes; 
    ...
};

CFRunLoop结构体中存在一个概念 ---commonModes。每当RunLoop的内容发生变化时,都会将commonModeItems中的Source/Observer/Timer同步到所有标记为“common”的mode中。而苹果提供的NSRunLoopCommonModes字符串就是用来操作 Common Items。

而在主线程中的UITrackingRunLoopMode和kCFRunLoopDefaultMode都已标记为“common” mode。

所以换个角度想就是:
将NSTimer添加到NSRunLoopCommonModes中等价于添加到commonModeItems中,然后commonModeItems会将Source/Observer/Timer同步到UITrackingRunLoopMode和kCFRunLoopDefaultMode中。

2 Categroy

深入理解Objective-C:Category
美团的这篇文章从结构体c++解析方面来揭露了category的底层。其实项目中很频繁的会使用到category,主要是为了将主类中不同模块的实现方法放在不同的类中,这样有减少单个文件的体积等好处。我相信不只我一个人有疑惑:

为什么Category中是无法添加实例变量?

首先,来看看Category的结构体:

typedef struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;  //实例方法
    struct method_list_t *classMethods;       //类方法
    struct protocol_list_t *protocols;              //协议
    struct property_list_t *instanceProperties;     //属性
} category_t;

从定义中可以明白category能创建实例方法、类方法和协议、属性,就是无法创建实例变量。
其次,来看看Runtime中关于类的结构体:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class   OBJC2_UNAVAILABLE;
    const char *name   OBJC2_UNAVAILABLE;
    long version     OBJC2_UNAVAILABLE;
    long info     OBJC2_UNAVAILABLE;
    long instance_size   OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars  OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists  OBJC2_UNAVAILABLE;
    struct objc_cache *cache    OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols   OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

在运行期间类的内存布局已经确定了,如果添加实例变量,就会破坏了类的结构。而能添加方法的原因是:
methodLists 是指向 objc_method_list 指针的指针,也就是说可以动态修改 *methodLists 的值来添加成员方法。

3 CoreData

在应用上线前,怎么修改CoreData中的表结构以及数据的增减都是没有任何问题的,最怕的是:
当应用版本更新,之前的CoreData数据库中已经存在数据了,而现在需要修改表中的一个字段或者添加一张新表等,该如何做?

①更改model中的实体之后,如何进行数据迁移

举例说明:
当前版本的数据库中comicC(C公司的漫画)


数据结构字段

现在修改的2.0版本中C公司的漫画全都导入JP公司的漫画库了,这时候不能直接修改字段名,也不能丢弃之前用户存在本地关于C公司的漫画,所以:

第一步 创建2.0版本的model

点击 Editor --> add Model Version,基于当前的model创建一个2.0版本的model。

添加2.0版本的model

在添加了2.0版本的model之后,根据自己的需求设置model内的实体结构,这样才能保证在不更改之前版本的model结构上添加新的字段或者实体。

设置2.0版本model的结构

设置完成之后,一定要注意,这个时候项目中使用的model还未切换到model 2,必须切换model的版本。

将model 2设置为项目使用model

然后就是代码处理了,根据自己的需求可以分别使用以下的方法:

如果两个版本中的字段相近,可以设置为系统自动判断
  NSDictionary *options = @{
                                          //设置为YES,coredata会试着把低版本的数据存储区切换到最新的版本中model 2
                                          NSMigratePersistentStoresAutomaticallyOption : @YES,
                                          //试着以最合适的方式自动推断出原型实体model里的某个属性到底对于model2中的哪一个属性
                                          NSInferMappingModelAutomaticallyOption : @YES
                                          };
如果两个版本中的字段相差较远,手动设置:
第一步 将系统自动判断的选项关闭
 NSInferMappingModelAutomaticallyOption : @NO
第二步 创建model 到 model2之间的Mapping model
创建MappingModel

并且将 model ------>设置为source Data Model(数据资源来源的Model)
model2 ---->设置为Target Data Model(数据迁移的目标Model)

创建MappingModel结果

这就手动的告知了系统关于各个字段之间的匹配关系。其实就相当于切换了model,将数据库中的字段或者结构关系改变了。


② 如何处理数据的预加载问题

这是我刚学会的一种实现思路:用户在多次进入应用的时候会留下响应的信息存储,这将加快应用的开启,但是当用户第一次进入应用时:

步骤一 在本地设置一个默认的plist文件(xml或sqlite)
设置一个本地默认数据文件

进入文件结构中:

数据文件结构

最重要的一点就是对版本versionNumber的判断。

步骤二 判断用户是否第一次进入应用

[[self filePath] checkPromisedItemIsReachableAndReturnError:nil]

如果用户第一次安全该版本的应用,需要加载成本地的默认数据;其他情况 --->非该版本或者非第一次进入都直接加载用户保存的数据:

 NSDictionary *dicPlist = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"ComicList" ofType:@"plist"] ];
        NSLog(@"plist ---%@",dicPlist);
        
        if ([self checkIfDataNeedsImport:[self filePath] ofType:NSSQLiteStoreType withCurrentVersion:[dicPlist objectForKey:@"versionNumber"]]) {
            [self setDataAsImportForStore:_store withVersion:[dicPlist objectForKey:@"versionNumber"]];
            
            NSArray *array = [dicPlist objectForKey:@"items"];
            //选择插入
            for (NSDictionary *item in array) {
                NSString *entity = [item objectForKey:@"Entity"];
                NSDictionary  *comic = [item objectForKey:@"Comic"];
                BOOL existing = [self existingObjectInContext:_context object:entity attributes:comic];
                //如果不存在的话
                if (existing) {
                    NSManagedObject *newObject = [NSEntityDescription insertNewObjectForEntityForName:entity inManagedObjectContext:_context];
                    
                    [newObject setValuesForKeysWithDictionary:comic];
                    NSLog(@"插入成功");
                } else {
                    NSLog(@"已经存在了,不用管了");
                }
            }
            [self saveContext];
        }

其实对一些复杂点的数据,直接建立一个默认的sqlite存在本地是更好的选择。


3 总结

其实都是基于一个机会发现很多底层的东西都不是特别明白,所以将一些容易搞混的东西记录下来,以供日后参考。接下来还会在项目之余更新一些底层的参考资料和推荐的处理思路。
如果有更好的思路可以和我交流。

iOS每周一结
Web note ad 1