The Weekend Blog

I hope it will continue to be updated

iOS代码规范

1.命名规范

你的代码可能会被任何人阅读,而阅读的人来自不同的地方接受不同的教育不同的文化。

苹果的API设计命名都是非常好的参照典范,如果不知道如何设计命名,请多阅读文档。

1.通用命名规范

  • 清晰性:好的命名应该是能自我描述的。
 //正例:
removeObject:、[string stringByReplacingOccurrencesOfString:@"1" withString:@"2"]
//反例:
remove:(不清楚,要删除什么?)、string.replace("1", "2")
  • 一致性:命名应该和上下文乃至全局保持一致性,相同类型或者具有相同作用的变量的命名方式应该相同或类似。
 // 正例:
NSDictionary、NSArray、NSSet这几个集合类都是用count来表示数量而不是一个用count其它的用amount或其他单词,这体现了命名的一致性。
@property (readonly) NSUInteger count;
  • 禁止自我指涉:命名不要自我指涉。通知、掩码常量等除外(通常指那些可以进行按位运算的枚举值)。
 // 正例:
NSString
 // 反例:
NSStringObject
  • 杜绝过度缩写,严禁自创缩写(例如把button缩写为btn);国际通用缩写名称除外(例如ATM、GPS)。
 // 正例:
destinationSelection、setBackgroundColor
// 反例:
destSel、setBgColor
  • 杜绝无意义的拼音,国际通用名称或者地名人名除外(例如alibaba、taobao、hangzhou)。
 // 反例 中英结合:
DaZhePromotion(打折促销) 
  • 命名要尽可能的清晰并简洁,如果两者不能兼得,则以清晰为主。
// 正例:
insertObject:atIndex:
// 反例:
insert:at:(不清晰,插入什么?at代表什么?)
  • 代码和注释中都要避免使用任何语言的种族歧视性词语。

  • 类名、协议名、函数名、常量名、枚举名等一些全局命名需要添加前缀,前缀需要大于2个字符且全部大写。

 Tips: 系统保留任意两个字符作为前缀的使用权,
包括但不限于NS、UI、CG、CF、CA、WK、MK、CI、NC;前缀若等于2个字符可以考虑添加_。
正例:
ZT_LoginViewController
反例:
ZTLoginViewController
  • 类名、协议名、函数名、常量名、枚举名等一些全局命名遵循首字母大写的驼峰命名方式,首个单词是HTTP这种特殊词除外。

  • 方法名、属性名等一些非全局命名遵循首字母小写的驼峰命名方式命名,首个单词是HTTP这种特殊词除外。

  • 成员变量需要以_开头。

 // 正例:
NSString *_nameString;
  • 在给常量或变量命名时,尽量将表示类型的名词放在词尾,以提升辨识度。
 // 正例:
nameLabel、nameString
// 反例:
name(name是字符串还是什么?)
  • 如果模块、接口、类、方法使用了模式,在命名时尽量体现出具体模式。
 正例:
OrderFactory、LoginProxy
  • 局部临时变量命名可以加上标识符作为前缀。
 // 正例:
t_label、t_string(t在这里表示temp)

2.类命名规范

  • 类名命名风格由"前缀+类的名称+类的类型"3个部分组成,前缀必须大于2个字符且全部大写(如果等于2个字符可以添加_);

  • 类的名称遵循首字母大写驼峰式命名,类的名称要能表达出该类的功能;

  • 类的类型必须使用全称,严禁使用缩写(例如vc代替viewController,cell代替TableViewCell),命名方式和名称命名一样首字母大写。

  // 正例:
 WXYZ_LoginViewControler 
 WXYZ_表示前缀,
 Login表示该类跟登录相关,
 ViewController表示该类是一个视图控制器而不是View。

3.方法命名规范

  • 所有方法名称禁止以new开始。

  • 所有方法名称禁止使用开始。(系统会使用开头命名一些系统私有方法)

  • 内部私有方法需要增加前缀,前缀需要保持唯一性(例如p_)。给私有方法加前缀有2个好处:

    • 增加辨识度,提高代码可读性。
    • 避免自己的方法无意间覆盖了系统/框架同名的私有方法。
  • 如果方法返回接收者的某个属性值,那么请直接使用属性名作为方法名。

 // 正例:
- (CGSize)cellSize;
// 反例:
- (CGSize)getCellSize;
  • 如果方法间接返回一个或多个值,那么请用"getXXX"命名,这种命名只适用于返回多个数据项的情况。
 正例:
- (void)getCachedResponseForDataTask:(NSURLSessionDataTask *)dataTask 
                 completionHandler:(void (^) (NSCachedURLResponse * __nullable cachedResponse))completionHandler;
  • 方法的每个参数前都必须添加关键字。
 // 正例:
- (void)sendAction:(SEL)aSelector toObject:(id)anObject forAllCells:(BOOL)flag;
// 反例:
- (void)sendAction:(SEL)aSelector :(id)anObject :(BOOL)flag;
  • 参数之前的单词尽量能描述参数的意义。
 // 正例:
- (id)viewWithTag:(NSInteger)aTag;
// 反例:
- (id)taggedView:(int)aTag;
  • 请不要使用and连接接收者属性,尽管and读起来还算顺口,但随着你创建的方法参数的增加,这将会带来一系列的问题。
 正例:
- (int)runModalForDirectory:(NSString *)path file:(NSString *) name types:(NSArray *)fileTypes;
反例:
- (int)runModalForDirectory:(NSString *)path andFile:(NSString *)name andTypes:(NSArray *)fileTypes;
  • 如果方法描述了两个独立的动作,则可以使用"and"连接起来。
 正例:
- (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag;

4.Protocol命名规范

  • Protocol中的方法命名以触发消息的对象名开头,省略类名前缀并首字母小写,如果它没有关联任何类则可以忽略这个规则。
正例:
- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename;
  • 除非Protocol方法只有一个参数,否则冒号需紧跟在类名后面。
正例:
- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
- (BOOL)applicationOpenUntitledFile:(NSApplication *)sender;

5.Category命名规范

  • 分类命名也要和类命名一样添加前缀。
 正例:
UIView (YYAdd)
反例:
UIView (Add)
  • 分类中声明的方法名都要加上前缀。

  • Category中尽量不要声明属性,能挪尽量挪到主类中声明。

  • 如果一个类比较复杂,那么建议使用分类组织代码(可以参考系统的UIView)。

6.Notification命名规范

  • Notification的命名风格由"类名前缀" + “通知事件名称” + “Notification"3个部分组成。
 正例:
UIApplicationDidBecomeActiveNotification
UIApplication表示该通知属于谁,
DidBecomeActive表示该通知的作用,
Notification表示它是一个通知。
  • 如果一个类声明了delegate属性,通常情况下,这个类的delegate对象应该可以通过实现的delegate方法收到大部分通知消息。例如applicationDidBecomeActive:代理方法和NSApplicationDidBecomeActiveNotification通知(这其实也符合命名规范的基本原则"一致性")。

7.常量命名规范

  • 如果常量局限于某"编译单元"之内,通常在前面加小写字母k作为前缀,若常量在全局可见,通常以类名作为前缀,然后采用首字母大写的驼峰式命令风格。
 正例:
// 局部可见
const CGFloat kAnimationDuration = 2.0;
// 全局可见
const CGFloat UIActivityIndicatorViewAnimationDuration = 2.0;

8.Exception命名规范

  • 命令规范和Notification一样,把后缀改为"Exception"即可。

9.文件(图片)命名规范

  • 文件名全部小写。
  • 采用_连接单词。
  • 命名的风格:"模块_属性描述",可根据项目适当增加描述。
 正例:
public_back@2x.png

2.编码规范

1.通用编码规范

  • 如果有使用到CF(Core Foundation)等框架时,或者是在iOS10以下系统使用通知和KVO时,切记在dealloc方法中释放对象以及移除通知和监听。

  • 在dealloc方法内禁止将self传递出去,如果self被retain,到下个runloop周期再释放则会多次释放导致crash。

  • 禁止使用过时的方法或类,应该及时去了解和使用新方法或类。使用新方法时建议了解一下为什么废弃掉旧方法/类。

  • 对剪切板的读取操作必须放在子线程中进行,因为用户可能在Mac上复制大量数据然后通过iCloud同步到iPhone上。

  • if、else、for、while、case等后面必须要有{},除非后面是简单的return类型语句,例如if (xxx) return;

  • 当方法可能会提前return时,需要要注意对象的释放问题,避免内存泄漏。

 反例:
CFArrayRef arrayRef = (__bridge CFArrayRef)array;
if (x == YES) return;
CFRelease(arrayRef);
以上代码如果x等于YES的话那么arrayRef对象就会内存泄漏。
  • 当使用@try处理异常时,需要要注意对象的释放问题,避免内存泄漏。同上
 反例:
@try {
CFArrayRef arrayRef = (__bridge CFArrayRef)array;
//do some thing……
CFRelease(arrayRef);    
} @catch (NSException *exception) {
}
以上代码如果do some thing……出现异常的话那么arrayRef就会出现内存泄漏。
  • 声明常量请使用const类型声明,禁止使用#define宏定义。

  • 宏定义声明常量的缺点:

    • 宏定义只是简单的替换,缺少编译检查,运行期可能会出现溢出或数据错误等问题。
    • 宏定义缺少类型,不方便编写文档用例。
    • 宏定义可能会被不小心替换。
    • 宏定义无法编写注释。
 反例:
#define kTime @"10"
if (1 == 2) {
#define kTime @"20"
}
NSLog(@"time = %@", kTime);
以上代码中的if永远不会执行,但是编译器也会将kTime替换为@"20"
  • 写一些公共方法时,请尽量使用内联函数或者全局函数而不是宏定义。函数不通过对象调用,所以不会走OC的消息转发流程,效率远高于方法调用;而且函数会有返回值和参数类型以及参数检查,而这些都是宏定义没有的。
正例:
UIKIT_STATIC_INLINE NSString * kNSStringFromInteger(NSInteger a) {
  return [NSString stringWithFormat:@"%zd", a];
}
反例:
#define kNSStringFromInteger(a) [NSString stringWithFormat:@"%zd", a]
  • UITableView使用self-sizing实现不等高cell时,请在tableView:cellForRowAtIndexPath:代理方法中给cell设置数据而不是tableView:willDisplayCell:forRowAtIndexPath:代理方法中设置数据。

  • 只在必要的时刻使用懒加载。

    • 对象的创建需要依赖其他对象
    • 对象可能被使用,也可能不被使用
    • 对象创建比较消耗性能
  • 懒加载方法内应该只执行需要初始化的操作,不应该有其他不必要的逻辑代码。

  • 使用一目运算符时左右两边不能有空格。

 正例:
i++、++i、
反例:
i ++、++ i
  • 使用二目、三目运算符时左右两边必须有且仅有一个空格。
 正例:
1 + 2
反例:
1+2
  • 采用4个空格缩进,如果要使用Tab字符,请将1个Tab设置成4个空格。

  • 使用NSUserDefaults存储数据时禁止调用synchronize方法,因为系统会在合适的时机将数据保存到本地(即使程序闪退等极端情况)。

  • 添加到集合中的对象应该是不可变的,或者在加入之后其哈希码是不可变的。

 反例:
NSMutableSet *sets = [NSMutableSet set];
NSMutableString *string1 = [NSMutableString stringWithString:@"1"];
[sets addObject:string1];
[sets addObject:@"12"];
[string1 appendString:@"2"];
当 [string1 appendString:@"2"] 执行完以后sets对象内会包含2个@"12"。
  • 必须使用CGRectGet获取Frame的各种值,而不是通过frame.的方式获取。
 CGRect t_frame = CGRectMake(-10, -10, -10, -10);
当一个view的frame设置成t_frame后,其坐标会隐式的转换为CGRectMake(-20, -20, 10, 10),因为宽高不可能出现负值;这时通过t_frame.的方式获取的值都是错误的,而CGRectGet会自动帮您处理这些隐式转换。
正例:
CGRectGetWidth(frame)、CGRectGetMinX(frame)、CGRectGetMaxX(frame)
反例:
frame.size.width、frame.origin.x、frame.size.width + frame.origin.x
  • 单行字符数限制不超过150个,超出需要换行(空格可以除外),换行时遵循如下原则:
    • 第二行相对第一行缩进4个空格,从第三行起不再继续缩进。
    • 运算符与下文一起换行。
    • 方法调用的点符号与下文一起换行。
 正例:
- (void)setImageWithURL:(nullable NSURL *)imageURL
          placeholder:(nullable UIImage *)placeholder
              options:(YYWebImageOptions)options
             progress:(nullable YYWebImageProgressBlock)progress
             ransform:(nullable YYWebImageTransformBlock)transform
           completion:(nullable YYWebImageCompletionBlock)completion;
  • 不可变对象尽量使用copy修饰,如果重写使用copy修饰的set方法,请注意调用copy方法。

  • 对于一些体积小并且重要的信息,不要频繁的存储到本地,可以使用NSUserDefaults进行存储。它会在合适的时机存储到本地,这避免了频繁的写入操作,而且在某些极端情况下它也能保证数据存储到本地(例如程序闪退等情况)。

  • 在多线程环境下谨慎使用可变集合,必要时候可以采用加锁或GCD的同步线程进行保护,或者在访问可变集合时先将其copy为不可变对象然后再对其访问。

  • 头文件中尽量不要声明成员变量而是使用属性代替。

  • 头文件中的属性尽量声明为只读,可以在实现文件中再将属性声明为可读可写。

  • 不要使用一个类去维护多个类的内容,例如一个常量类维护所有的常量类,要按常量功能进行归类,分开维护。

 正例:
缓存相关常量类放在CacheCosts下,系统配置相关常量类放在SystemConfigConsts下。
  • 如果大括号内为空,则简洁的写成{}就行。

  • 没有必要增加多余空格来使上下代码的等号对齐。

 // 正例:
int a1 = 1;
long a2 = 3;
NSString *a3 = @"";
// 反例:
int a1       = 1;
long a2      = 3;
NSString *a3 = @"";
  • 少用if else,可以使用 if return 替换,if 嵌套最好不超过5层。
 // 正例:
if (x == 1) {
……
return;
}
if (x == 2) {
……
return;
}
// 反例:
if (x == 1) {
……
} else if (x == 2) {
……
}
  • 尽量避免采用取反逻辑运算符,因为取反逻辑不利于快速理解。
 // 正例:
if (array == nil) {
……
}
// 反例:
if (!array) {
……
}
  • 如果用到了很多协议,必要时可以把协议封装到一个单独的头文件中,这样做不仅可以减小编译时间,还能避免循环引用。

  • 使用Switch枚举时尽量将所有枚举类型都列举出来而不使用default,这样下次增加枚举类型时如果Switch没有处理会有警告信息。

  • 尽量使用字面量语法创建对象,少用与之等价的方法。

  • 优点
    • 简单易读,提高代码的可读性和可维护性。
    • 使用字面量创建数组、字典时如果元素里在nil则会抛出异常,而使用arrayWithObjects:这些等价方法创建则会丢失nil后的数据,抛出异常能让你知道这里有问题及时修改防止问题在线上发生。
  • 缺点

    • 使用字面量创建的对象默认是不可变的,如果要创建可变对象需要进行mutableCopy操作。
    • 不支持子类,如果你创建了一个NSString的子类,@“"并不会返回你想要的子类对象。
  • 头文件中尽量少引用其他头文件,尽量使用@class向前声明,每次引入其他头文件时问问自己是否必须要这样做。

  • UI控件建议使用weak修饰而不是strong修饰。

2. 类编码规范

  • 如果超类的某个初始化方法不适用于子类,那么子类一定要覆写超类的这个方法并解决该问题或抛出异常。

  • 尽量不要使用load类方法,如果必须要使用不能在方法内实现复杂逻辑或堵塞线程。

  • 尽量减少继承,类的继承尽量不要超过3层,必要时刻可以考虑用分类、协议来代替继承。

  • 把一些稳定的、公共的变量或者方法抽取到父类中。子类尽量只维持父类所不具备的特性和功能。

3. 方法编码规范

  • 禁止在init等初始化方法内部、getter、setter、dealloc或其他特殊地方使用.语法访问属性。

  • 方法参数在定义和传入时,逗号后面必须添加一个空格。

 // 正例:
method(a1, a2, a3);
  • 单个方法的行数建议不超过80行,注释、左右大括号、空行、回车等除外。

  • 在实现文件内部也尽量使用.语法访问属性而不是使用_直接访问成员变量来保证风格统一。


4. Block编码规范

  • 调用Block必须判空处理。
 对于简单的Block可以使用三目运算进行判空处理,
例如 !self.block ?: self.block();
  • 在Block内部使用外部变量时要注意循环引用的问题。
    • 不一定在Block内使用self才会循环引用,如下情况也会造成循环引用:
     - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    WXYZ_TitleTableViewCell *cell = ………
    cell.refreshTableViewBlock = ^{
       [tableView reloadData];
    };
    return cell;
    }
    
    • Block内部是否要使用weak需要看Block本身和weak的这个对象是否存在直接或间接的相互引用,若无相互引用则不需要使用weak。

    • 如果Block内部使用了strong修饰了外部的weak变量,那么当使用strong指向成员变量时需要进行判空,否则会崩溃

     __weak typeof(self) weakSelf = self;
    cell.refreshTableViewBlock = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf != nil) {
            strongSelf->_name = @"name";
        }
    };
    

5. 通知编码规范

  • 在发送通知时,请使用userInfo进行传值,而不是object。

  • 通知中心是以同步的方式发送请求的,所以不要在通知方法做一些复杂的计算,特别是当它处于主线程的时候,如果想发送异步通知可以使用NSNotificationQueue。

  • 在工程里能不用通知尽量不用通知,通知虽然灵活强大,但是如果滥用会导致工程质量下降,出现问题时也比较难排查。

6. 注释编码规范

  • 注释是起辅助作用的,清晰的命名可以不需要注释,注释应该帮助别人更快的理解该方法的使用和注意事项

  • 当修改了方法实现时需要同步修改注释内容。

  • 注释不要写的太冗长,要简单易读容易理解。

  • 注释的双斜线和内容之间有且仅有一个空格。

  • 对于代码注释需谨慎,代码被注释一般有2种可能,

    • 1) 后续会恢复此段代码逻辑;需添加相应注释
    • 2) 永久不用;建议直接删除。
  • 别给糟糕的代码加注释,重构它。

3.工程结构规范

  • 局部使用的常量、静态变量声明在@interface之前。

  • @property同一类型的声明放在一块,不同类型的声明用2行空格隔开。

  • 不同逻辑、不同语义、不同业务的代码之间插入一个空行分隔开以提升可读性。

  • 方法归类

 #pragma mark - LifeCycle(生命周期相关的代码放在最上面)
- (void)dealloc {}
- (void)viewDidLoad {}
- (void)viewWillAppear:(BOOL)animated {}
#pragma mark - Public(公开方法)
// code...
// 上空一行
// 下空两行
#pragma mark - Private(私有方法)
//
#pragma mark - Override(需要覆盖父类的方法)
//
#pragma mark - Notification(通知方法)
//
#pragma mark - Delegate(Delegate需要实现的方法)
//
#pragma mark - getter/setter

KVC和KVO

KVC (Key-Value-Coding)

[TOC]

重要API:

1
2
// 默认返回YES,如不想使用KVC,可手动返回NO
@property (class, readonly) BOOL accessInstanceVariablesDirectly;
1
2
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
1
2
- (nullable id)valueForKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;

setValue顺序如下:

  1. 查找是否有-set<Key>:。如果有,判断参数类型如果不是对象指针类型(不是NS开头)并且value是nil,则会调用-setNilValueForKey:方法,该方法会引起NSInvalidArgumentException,也就是崩溃,但是可以重写这个方法。NSNumber/NSValue类型会做一些转换。
  2. 否则,当没有找到set方法时,如果accessinstancevariablesdirect返回YES,则查找_<key>_is< key>, <key>,或is< key>的实例变量。如果找到了这样一个实例变量,并且它的类型是对象指针类型,结果将在实例变量中设置。如果实例变量的类型是其他类型,参照步骤1
  3. 否则(没有找到访问方法或实例变量),调用-setValue:forUndefinedKey:-setValue:forUndefinedKey的默认实现会引发NSUndefinedKeyException,但是你可以在你的应用程序中重写它。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
简单定义一个Object的结构
@interface KVCTest : NSObject{
    Rect rect;
    NSValue *value;
    int i;
    NSInteger age;
}

@property (nonatomic, copy) NSString *name;

@end
调用
 KVCTest *test = [KVCTest new];
 // nil和非nil不会有问题
    [test setValue:nil forKey:@"name"];
 // nil会引起崩溃
    [test setValue:nil forKey:@"rect"];
 // 无问题
    [test setValue:@1 forKey:@"i"];
 // 无问题
    [test setValue:@2 forKey:@"age"];
    
 如重写以下方法
 -(void)setNilValueForKey:(NSString *)key{
    NSLog(@"不能将%@设成nil",key);
    // log:不能将rect设成nil
}


+(BOOL)accessInstanceVariablesDirectly{
    return NO;
}
如果设置为NO,实例变量都会不再寻找,会调用setValue:forUndefinedKey:方法,如果该方法没有被重新就会发生崩溃

valueForKey查找顺序

  1. 首先按照-get<Key>, -< Key>,或-is<Key>查找,如有则调用,如果结果是NSNumber可转换的类型,如int、bool则转换成NSNumber。
  2. 如果没找到,继续查找-countOf <key>-indexIn <key> OfObject:-objectIn <key> AtIndex:<Key>AtIndexes,如果找到了count方法和indexOf方法以及另外两个可能的方法中的至少一个,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf<Key>,objectIn<Key>AtIndex<Key>AtIndexes这几个方法组合的形式调用。还有一个可选的get<Key>:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。
  3. 如果上面的方法没有找到,那么会同时查找countOf<Key>enumeratorOf<Key>,memberOf<Key>格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf<Key>enumeratorOf<Key>,memberOf<Key>组合的形式调用。
  4. 否则,调用accessinstancevariablesdirect方法是否返回YES,如果是,则查找_<key>_is< key><key>,或is< key>的实例变量,按此顺序。如果找到了,返回规则同步骤1,如果没找到,则调用-valueForUndefinedKey:,并引发崩溃。

看文字不太好理解,举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 KVCTest *test = [KVCTest new];
 id ar = [test valueForKey:@"numbers"];
 NSLog(@"%@",NSStringFromClass([ar class]));
 // ar是NSKeyValueArray类型,属于NSArray的子类

 NSLog(@"%lu",(unsigned long)[ar count]);
  // 会调用 countOf<Key>
 -(NSUInteger)countOfNumbers{
    return self.count;
}

 NSLog(@"%@",ar[0]);
 // 会调用 -objectIn <key> AtIndex:
 -(id)objectInNumbersAtIndex:(NSUInteger)index{ 
    return @(index * 2);
}

NSInteger index =  [ar indexOfObject:@"123"];
// 会调用 -indexIn <key> OfObject:
-(NSUInteger)indexInNumbersOfObject:(id)object{
    return 0;
}
 

正确性验证

1
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

这个方法的默认实现是去探索类里面是否有一个这样的方法:-(BOOL)validate:error:如果有这个方法,就调用这个方法来返回,没有的话就直接返回YES

KVC应用场景

  • 使用KVC访问私有变量和属性,如UITextField中的placeHolderText
  • Model和字典转换
  • 函数操作集合@avg@count@max@min@sum
  • 传递方法到容器中的每一个对象,如uppercaseString: 全部大写,length: 获取每个字符串长度
  • 对象运算,@distinctUnionOfObjects:返回去重后结果(无序),@unionOfObjects返回全部结果

KVO (Key-Value-Observing)

// 待补充

App启动速度

一般情况下,App 的启动分为冷启动和热启动。

  • 冷启动是指, App 点击启动前,它的进程不在系统里,需要系统新创建一个进程分配给它启动的情况。这是一次完整的启动过程。
  • 热启动是指 ,App 在冷启动后用户将 App 退后台,在 App 的进程还在系统里的情况下,用户重新启动进入 App 的过程,这个过程做的事情非常少。

App 的启动时间,指的是从用户点击 App 开始,到用户看到第一个界面之间的时间。

App 的启动主要包括三个阶段:

  • main() 函数执行前;
  • main() 函数执行后;
  • 首屏渲染完成后。

main() 函数执行前

在 main() 函数执行前,系统主要会做下面几件事情:

  • 加载可执行文件(App 的.o 文件的集合);
  • 加载动态链接库,进行 rebase 指针调整和 bind 符号绑定;
  • Objc 运行时的初始处理,包括 Objc 相关类的注册、category 注册、selector 唯一性检查等;
  • 初始化,包括了执行 +load() 方法、attribute((constructor)) 修饰的函数的调用、创建 C++ 静态全局变量。

这个阶段对于启动速度优化来说,可以做的事情包括:

  • 减少动态库加载。每个库本身都有依赖关系,苹果公司建议使用更少的动态库,并且建议在使用动态库的数量较多时,尽量将多个动态库进行合并。数量上,苹果公司建议最多使用6个非系统动态库。
  • 动态库是指可以共享的代码文件、资源文件、头文件等的打包集合体。在Xcode->Targets->General->Link Binary With Libraries可以检查自己的库

  • 减少加载启动后不会去使用的类或者方法。

  • +load() 方法里的内容可以放到首屏渲染完成后再执行,或使用 +initialize() 方法替换掉。因为,在一个 +load() 方法里,进行运行时方法替换操作会带来 4 毫秒的消耗。不要小看这 4 毫秒,积少成多,执行 +load() 方法对启动速度的影响会越来越大。
1
2
3
4
5
6
7
8
9
10
11
12
执行顺序:
 load -> attribute((constructor)) -> main -> initialize
 
  + (void)initialize{
    // 在类被第一次使用的时候调用
  }
  + (void)load{
    // 在类被运行时加载时调用
  }
  __attribute((constructor)) void beforeMain(){
  //是GCC的扩展语法(黑魔法),由它修饰过的函数,会在main函数之前调用
  }
  • 控制 C++ 全局变量的数量。

查看main()函数执行前的耗时:

在Product->Scheme->Edit Scheme->Run->Arguments->Environment Variables->DYLD_PRINT_STATISTICS设置为YES,就可以在控制台中查看main函数执行前总共花费的多长时间。


main() 函数执行后

main() 函数执行后的阶段,指的是从 main() 函数执行开始,到 appDelegate 的 didFinishLaunchingWithOptions 方法里首屏渲染相关方法执行完成。

优化方向:从功能上梳理出哪些是首屏渲染必要的初始化功能,哪些是 App 启动必要的初始化功能,而哪些是只需要在对应功能开始使用时才需要初始化的。梳理完之后,将这些初始化功能分别放到合适的阶段进行。

alt text


首屏渲染完成后

首屏渲染后的这个阶段,指的是didFinishLaunchWithOptions方法作用域内执行首屏渲染之后的所有方法执行完成,即从 设置了self.window.rootViewController开始 到 didFinishLaunchWithOptions方法作用域结束。

这个阶段用户已经能够看到 App 的首页信息了,所以优化的优先级排在最后。但是,那些会卡住主线程的方法还是需要最优先处理的,不然还是会影响到用户后面的交互操作。


功能级别的启动优化

功能级别的启动优化,就是要从 main() 函数执行后这个阶段下手。

main() 函数开始执行后到首屏渲染完成前只处理首屏相关的业务,其他非首屏业务的初始化、监听注册、配置文件读取等都放到首屏渲染完成后去做。

方法级别的启动优化

首先优化前需要先精准监控到都哪些方法需要优化,就是哪些方法更耗时 对 App 启动速度的监控,主要有两种手段。

  • 第一种方法是,定时抓取主线程上的方法调用堆栈,计算一段时间里各个方法的耗时。Xcode 工具套件里自带的 Time Profiler ,采用的就是这种方式。
  • 第二种方法是,对 objc_msgSend 方法进行 hook 来掌握所有方法的执行耗时。

第一种方法

Time Profiler

Time Profiler每隔1ms会对线程的调用栈采样,然后用统计学的方式去做出分析。
Time Profiler用来分析代码的执行时间,主要用来分析CPU使用情况。 注意:要在release模式下分析。

缺点:

  • 定时间隔设置得长了,会漏掉一些方法,从而导致检查出来的耗时不精确;
  • 而定时间隔设置得短了,抓取堆栈这个方法本身调用过多也会影响整体耗时,导致结果不准确。

这个定时间隔如果小于所有方法执行的时间(比如 0.002 秒),那么基本就能监控到所有方法。但这样做的话,整体的耗时时间就不够准确。一般将这个定时间隔设置为 0.01 秒。这样设置,对整体耗时的影响小,不过很多方法耗时就不精确了。但因为整体耗时的数据更加重要些,单个方法耗时精度不高也是可以接受的,所以这个设置也是没问题的。

总结来说,定时抓取主线程调用栈的方式虽然精准度不够高,但也是够用的。

实现类似Time Profiler的检测工具

要获取线程的调用栈,github上有一个开源轻量级工具BSBacktraceLogger

第二种方法实现

主要是搬戴铭大佬的成果:
objc_msgSend 本身是用汇编语言写的,这样做的原因主要有两个:

  • 一个原因是,objc_msgSend 的调用频次最高,在它上面进行的性能优化能够提升整个 App 生命周期的性能。而汇编语言在性能优化上属于原子级优化,能够把优化做到极致。所以,这种投入产出比无疑是最大的。
  • 另一个原因是,其他语言难以实现未知参数跳转到任意函数指针的功能。

objc_msgSend 方法执行的逻辑是:先获取对象对应类的信息,再获取方法的缓存,根据方法的 selector 查找函数指针,经过异常错误处理后,最后跳到对应函数的实现。

怎么hook objc_msgSend 方法?

Facebook 开源了一个库,可以在 iOS 上运行的 Mach-O 二进制文件中动态地重新绑定符号,这个库叫 fishhook

fishhook 实现的大致思路是,通过重新绑定符号,可以实现对 c 方法的 hook。dyld 是通过更新 Mach-O 二进制的 __DATA segment 特定的部分中的指针来绑定 lazy 和 non-lazy 符号,通过确认传递给 rebind_symbol 里每个符号名称更新的位置,就可以找出对应替换来重新绑定这些符号。

有了fishhook后还需要实现两个方法 pushCallRecord 和 popCallRecord,来分别记录 objc_msgSend 方法调用前后的时间,然后相减就能够得到方法的执行耗时。

具体实现参考戴铭老师的github中的SMCallTrace

预编译指令

预编译指令

以#开头的都是预编译指令,就是在正式编译之前,编译器做一些预处理的工作

预编译指令 含义
#define 定义一个预处理宏
#undef 取消宏的定义
#if 编译预处理中的条件命令,相当于if语句
#ifdef 判断某个宏是否被定义,若已定义,执行随后的语句
#ifndef 与#ifdef相反,判断某个宏是否未被定义
#elif 若#if, #ifdef, #ifndef或前面的#elif条件不满足,则执行#elif之后的语句,相当于else-if
#else 与#if, #ifdef, #ifndef对应, 若这些条件不满足,则执行#else之后的语句,相当于else
#endif #if, #ifdef, #ifndef这些条件命令的结束标志.
defined 与#if, #elif配合使用,判断某个宏是否被定义

代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 宏定义
#define ISOK
#define a 4
#define b 5
//取消宏定义
//#undef ISOK

// #if后面必须跟一个判断条件,如果需要判断是否有某个宏定义需要使用defined
#if defined(ISOK)  //if
    NSLog(@"#if==true");
#elif (a==b)  // else if
    NSLog(@"#elif==true");
#else // else 
    NSLog(@"#else==true");
#endif

    
#ifdef ISOK 
    NSLog(@"#ifdef:ISOK已有宏定义");
#else 
    NSLog(@"#ifdef:ISOK没有宏定义");
#endif
  
#ifndef ISOK
    NSLog(@"#ifndef:ISOK没有宏定义");
#else
    NSLog(@"#ifndef:ISOK已有宏定义");
#endif

一般在iOS中用到预编译指令的地方:

1.调试代码:

1
2
3
#ifdef DEBUG
// 调试代码 ,注意DEBUG宏是系统定义好的
#endif

2.统一定义设备相关信息

1
2
3
4
5
6
7
8
9
#define NavigationBar_HEIGHT 44

#define SCREEN_WIDTH ([UIScreen mainScreen].bounds.size.width)
#define SCREEN_HEIGHT ([UIScreen mainScreen].bounds.size.height)

#define IOS_VERSION [[UIDevice currentDevice] systemVersion] floatValue]

#define CurrentSystemVersion [UIDevice currentDevice] systemVersion]

3.统一定义某个全局方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**
 *    @brief  正确获取主线程
 */
#ifndef dispatch_main_async_safe
    #define dispatch_main_async_safe(block)\
        if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
            (block());\
        } else {\
            dispatch_async(dispatch_get_main_queue(), block);\
        }
#endif


/**
 *    @brief  在Debug模式显示日志,Release模式关闭日志,普通输出
 */
#ifdef DEBUG
#define Log( s, ... ) printf( "<%s:(%d)> %s %s\n\n", [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] cStringUsingEncoding:NSUTF8StringEncoding], __LINE__,__func__, [[NSString stringWithFormat:(s), ##__VA_ARGS__] cStringUsingEncoding:NSUTF8StringEncoding])
#else
#define Log( s, ... )
#endif

/**
 *    @brief  防止block循环引用
 */
#ifndef weakify
    #if TARGET_IPHONE_DEBUG
        #if __has_feature(objc_arc)
            #define weakify(object) @autoreleasepool{} __weak __typeof__(object) weak##_##object = object;
        #else
            #define weakify(object) @autoreleasepool{} __block __typeof__(object) block##_##object = object;
        #endif
    #else
        #if __has_feature(objc_arc)
            #define weakify(object) @try{} @finally{} {} __weak __typeof__(object) weak##_##object = object;
        #else
            #define weakify(object) @try{} @finally{} {} __block __typeof__(object) block##_##object = object;
        #endif
    #endif
#endif

#ifndef strongify
    #if TARGET_IPHONE_DEBUG
        #if __has_feature(objc_arc)
            #define strongify(object) @autoreleasepool{} __typeof__(object) object = weak##_##object;
        #else
            #define strongify(object) @autoreleasepool{} __typeof__(object) object = block##_##object;
        #endif
    #else
        #if __has_feature(objc_arc)
            #define strongify(object) @try{} @finally{} __typeof__(object) object = weak##_##object;
        #else
            #define strongify(object) @try{} @finally{} __typeof__(object) object = block##_##object;
        #endif
    #endif
#endif

/**
 *    @brief rgb颜色转换(16进制->10进制)
 */
 #define UIColorFromRGB(rgbValue) [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 green:((float)((rgbValue & 0xFF00) >> 8))/255.0 blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0]

/**
 *    @brief 通知
 */
#define POST_NOTIFY(__NAME, __OBJ, __INFO) [[NSNotificationCenter defaultCenter] postNotificationName:__NAME object:__OBJ userInfo:__INFO];

#define LISTEN_NOTIFY(__NAME, __OBSERVER, __SELECTOR) [[NSNotificationCenter defaultCenter] addObserver:__OBSERVER selector:__SELECTOR name:__NAME object:nil];

#define REMOVE_NOTIFY(__OBSERVER) [[NSNotificationCenter defaultCenter] removeObserver:__OBSERVER];

React学习笔记

入门教程

  • 状态提升:当你遇到需要同时获取多个子组件数据,或者两个组件之间需要相互通讯的情况时,需要把子组件的 state 数据提升至其共同的父组件当中保存。之后父组件可以通过 props 将状态数据传递到子组件当中。这样应用当中所有组件的状态数据就能够更方便地同步共享了。

  • 为了提高可读性,我们把返回的 React 元素拆分成了多行,同时在最外层加了小括号,这样 JavaScript 解析的时候就不会在 return 的后面自动插入一个分号从而破坏代码结构了。

  • 在 React 中,有一个命名规范,通常会将代表事件的监听 prop 命名为 on[Event],将处理事件的监听方法命名为 handle[Event] 这样的格式。

  • concat() 方法可能与你比较熟悉的 push() 方法不太一样,它并不会改变原数组,所以我们推荐使用 concat()

KEY

  • 每当一个列表重新渲染时,React 会根据每一项列表元素的 key 来检索上一次渲染时与每个 key 所匹配的列表项。如果 React 发现当前的列表有一个之前不存在的 key,那么就会创建出一个新的组件。如果 React 发现和之前对比少了一个 key,那么就会销毁之前对应的组件。如果一个组件的 key 发生了变化,这个组件会被销毁,然后使用新的 state 重新创建一份。

  • key 是 React 中一个特殊的保留属性(还有一个是 ref,拥有更高级的特性)。当 React 元素被创建出来的时候,React 会提取出 key 属性,然后把 key 直接存储在返回的元素上。虽然 key 看起来好像是 props 中的一个,但是你不能通过 this.props.key 来获取 key。React 会通过 key 来自动判断哪些组件需要更新。组件是不能访问到它的 key 的。

  • 我们强烈推荐,每次只要你构建动态列表的时候,都要指定一个合适的 key。如果你没有找到一个合适的 key,那么你就需要考虑重新整理你的数据结构了,这样才能有合适的 key。

函数组件

  • 如果你想写的组件只包含一个 render 方法,并且不包含 state,那么使用函数组件就会更简单。我们不需要定义一个继承于 React.Component 的类,我们可以定义一个函数,这个函数接收 props 作为参数,然后返回需要渲染的元素。函数组件写起来并不像 class 组件那么繁琐,很多组件都可以使用函数组件来写。

定义组件最简单的方式就是编写 JavaScript 函数:

1
2
3
4
5
6
7
8
9
10
 function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
ES6:
class Welcome extends React.Component {
  render() {
      return <h1>Hello, {this.props.name}</h1>;
  }
}
注意: 组件名称必须以大写字母开头。

Props的只读性

1
2
3
 function sum(a, b) {
  return a + b;
}
  • 这样的函数被称为“纯函数”,因为该函数不会尝试更改入参,且多次调用下相同的入参始终返回相同的结果。 相反,下面这个函数则不是纯函数,因为它更改了自己的入参:
1
2
3
function withdraw(account, amount) {
  account.total -= amount;
 }
  • React 非常灵活,但它也有一个严格的规则: 所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。

正确地使用 State

  • 不要直接修改 State,要使用setState()
  • State 的更新可能是异步的

出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。 因为 this.props 和 this.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。

1
2
3
4
 // Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});

要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:

1
2
3
4
5
6
7
 // Correct
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));
this.setState(state => ({
  isToggleOn: !state.isToggleOn
}));
  • State 的更新会被合并

当你调用 setState() 的时候,React 会把你提供的对象合并到当前的 state。

  • 这通常会被叫做“自上而下”或是“单向”的数据流。任何的 state 总是所属于特定的组件,而且从该 state 派生的任何数据或 UI 只能影响树中“低于”它们的组件。

事件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 class LoggingButton extends React.Component {
  handleClick() {
    console.log('this is:', this);
  }

  render() {
    // 此语法确保 `handleClick` 内的 `this` 已被绑定。
    return (
      <button onClick={() => this.handleClick()}>
        Click me
      </button>
    );
  }
}
  • 此语法问题在于每次渲染 LoggingButton 时都会创建不同的回调函数。在大多数情况下,这没什么问题,但如果该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染。我们通常建议在构造器中绑定或使用 class fields 语法来避免这类性能问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 class LoggingButton extends React.Component {
  // 此语法确保 `handleClick` 内的 `this` 已被绑定。
  // 注意: 这是 *实验性* 语法。
  handleClick = () => {
  //  class fields 
    console.log('this is:', this);
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}
1
2
 <button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
  • 在这两种情况下,React 的事件对象 e 会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。

列表 & Key

  • 一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串
  • 一个好的经验法则是:在 map() 方法中的元素需要设置 key 属性。

React.lazy

  • React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。
1
2
3
4
 使用之前:
import OtherComponent from './OtherComponent';
使用之后:
const OtherComponent = React.lazy(() => import('./OtherComponent'));
  • React.lazy 目前只支持默认导出(default exports)。如果你想被引入的模块使用命名导出(named exports),你可以创建一个中间模块,来重新导出为默认模块。这能保证 tree shaking 不会出错,并且不必引入不需要的组件。
1
2
3
4
5
6
7
8
 // ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
// MyComponent.js
export { MyComponent as default } from "./ManyComponents.js";
// MyApp.js
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));

Context

  • Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。
  • Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。
1
 const MyContext = React.createContext(defaultValue);
  • 创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。 只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。这有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。
1
2
<MyContext.Provider value={/* 某个值 */}>
MyClass.contextType = MyContext;
  • 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。
1
2
3
4
5
6
7
8
9
10
 class MyClass extends React.Component {
static contextType = MyContext;
render() {
let value = this.context;
/* 基于这个值进行渲染工作 */
}
}
<MyContext.Consumer>
{value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>
1
2
3
4
 const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中

错误边界

  • 错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI
  • 错误边界无法捕获以下场景中产生的错误:
  • 事件处理
  • 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数
  • 服务端渲染
  • 它自身抛出来的错误(并非它的子组件)

  • 如果一个 class 组件中定义了 static getDerivedStateFromError() 或 componentDidCatch() 这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。

1
2
3
4
5
6
7
8
9
   static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
  }

Refs 转发

  • Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
//3.React 传递 ref 给 forwardRef 内函数 (props, ref) => ...,作为其第二个参数。
const FancyButton = React.forwardRef((props, ref) => (
//4.我们向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性。
//5.当 ref 挂载完成,ref.current 将指向 <button> DOM 节点。
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// 1.我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
const ref = React.createRef();
// 2.我们通过指定 ref 为 JSX 属性,将其向下传递给 <FancyButton ref={ref}>。
<FancyButton ref={ref}>Click me!</FancyButton>;
  • 第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 ref。

  • 一个输出组件 props 到控制台的 HOC (高阶组件)示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return LogProps;
}

// 使用
class FancyButton extends React.Component {
  focus() {
    // ...
  }

  // ...
}

// 我们导出 LogProps,而不是 FancyButton。
// 虽然它也会渲染一个 FancyButton。
export default logProps(FancyButton);
  • 以上例子直接使用ref会挂载到logProps上,而不是FancyButton

  • 改进示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      const {forwardedRef, ...rest} = this.props;

      // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  // 注意 React.forwardRef 回调的第二个参数 “ref”。
  // 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
  // 然后它就可以被挂载到被 LogProps 包裹的子组件上。
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}

Fragments

  • React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。
1
2
3
4
5
6
7
8
9
render() {
  return (
    <React.Fragment>
      <ChildA />
      <ChildB />
      <ChildC />
    </React.Fragment>
  );
}
  • 你可以使用一种新的,且更简短的语法来声明 Fragments。它看起来像空标签:
1
2
3
4
5
6
7
8
9
10
class Columns extends React.Component {
  render() {
    return (
      <>
        <td>Hello</td>
        <td>World</td>
      </>
    );
  }
}
  • key 是唯一可以传递给 Fragment 的属性。

高阶组件HOC

  • 高阶组件是参数为组件,返回值为新组件的函数

  • 组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。

  • HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。

将不相关的 props 传递给被包裹的组件

  • HOC 为组件添加特性。自身不应该大幅改变约定。HOC 返回的组件与原组件应保持类似的接口。

HOC 应该透传与自身无关的 props。大多数 HOC 都应该包含一个类似于下面的 render 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
render() {
  // 过滤掉非此 HOC 额外的 props,且不要进行透传
  const { extraProp, ...passThroughProps } = this.props;

  // 将 props 注入到被包装的组件中。
  // 通常为 state 的值或者实例方法。
  const injectedProp = someStateOrInstanceMethod;

  // 将 props 传递给被包装组件
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}

最大化可组合性

包装显示名称以便轻松调试

  • 请选择一个显示名称,以表明它是 HOC 的产物。
  • 最常见的方式是用 HOC 包住被包装组件的显示名称。比如高阶组件名为 withSubscription,并且被包装组件的显示名称为 CommentList,显示名称应该为 WithSubscription(CommentList):
1
2
3
4
5
6
7
8
9
function withSubscription(WrappedComponent) {
  class WithSubscription extends React.Component {/* ... */}
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
  return WithSubscription;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

HOC注意事项

  • 不要在 render 方法中使用 HOC
1
2
3
4
5
6
7
render() {
  // 每次调用 render 函数都会创建一个新的 EnhancedComponent
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
  return <EnhancedComponent />;
}

这不仅仅是性能问题 - 重新挂载组件会导致该组件及其所有子组件的状态丢失。 你可以在组件的生命周期方法或其构造函数中进行调用。

  • 务必复制静态方法 当你将 HOC 应用于组件时,原始组件将使用容器组件进行包装。这意味着新组件没有原始组件的任何静态方法。
1
2
3
4
5
6
7
// 定义静态函数
WrappedComponent.staticMethod = function() {/*...*/}
// 现在使用 HOC
const EnhancedComponent = enhance(WrappedComponent);

// 增强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true

为了解决这个问题,你可以在返回之前把这些方法拷贝到容器组件上:

1
2
3
4
5
6
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // 必须准确知道应该拷贝哪些方法 :(
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}

另一个可行的方案是再额外导出这个静态方法。

1
2
3
4
5
6
7
8
9
// 使用这种方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...单独导出该方法...
export { someFunction };

// ...并在要使用的组件中,import 它们
import MyComponent, { someFunction } from './MyComponent.js';
  • Refs 不会被传递

JSX

实际上,JSX 仅仅只是 React.createElement(component, props, …children) 函数的语法糖。

1
2
3
4
5
6
7
8
9
<MyButton color="blue" shadowSize={2}>
  Click Me
</MyButton>
会编译为:
React.createElement(
  MyButton,
  {color: 'blue', shadowSize: 2},
  'Click Me'
)

React 必须在作用域内

由于 JSX 会编译为 React.createElement 调用形式,所以 React 库也必须包含在 JSX 代码作用域内。 在如下代码中,虽然 React 和 CustomButton 并没有被直接使用,但还是需要导入:

1
2
3
4
5
6
7
import React from 'react';
import CustomButton from './CustomButton';

function WarningButton() {
  // return React.createElement(CustomButton, {color: 'red'}, null);
  return <CustomButton color="red" />;
}

在 JSX 类型中使用点语法

1
2
3
4
5
6
7
8
9
10
11
import React from 'react';

const MyComponents = {
  DatePicker: function DatePicker(props) {
    return <div>Imagine a {props.color} datepicker here.</div>;
  }
}

function BlueDatePicker() {
  return <MyComponents.DatePicker color="blue" />;
}

用户定义的组件必须以大写字母开头

以小写字母开头的元素代表一个 HTML 内置组件,比如 <div> 或者 <span>会生成相应的字符串 ‘div’ 或者 ‘span’ 传递给 React.createElement(作为参数)。大写字母开头的元素则对应着在 JavaScript 引入或自定义的组件,如 会编译为 React.createElement(Foo)。

我们建议使用大写字母开头命名自定义组件。如果你确实需要一个以小写字母开头的组件,则在 JSX 中使用它之前,必须将它赋值给一个大写字母开头的变量。

在运行时选择类型

你不能将通用表达式作为 React 元素类型。如果你想通过通用表达式来(动态)决定元素类型,你需要首先将它赋值给大写字母开头的变量。这通常用于根据 prop 来渲染不同组件的情况下:

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
  photo: PhotoStory,
  video: VideoStory
};

function Story(props) {
  // 错误!JSX 类型不能是一个表达式。
  return <components[props.storyType] story={props.story} />;
}

要解决这个问题, 需要首先将类型赋值给一个大写字母开头的变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
  photo: PhotoStory,
  video: VideoStory
};

function Story(props) {
  // 正确!JSX 类型可以是大写字母开头的变量。
  const SpecificStory = components[props.storyType];
  return <SpecificStory story={props.story} />;
}

JSX 中的 Props

JavaScript 表达式作为 Props

你可以把包裹在 {} 中的 JavaScript 表达式作为一个 prop 传递给 JSX 元素。

1
<MyComponent foo={1 + 2 + 3 + 4} />

if 语句以及 for 循环不是 JavaScript 表达式,所以不能在 JSX 中直接使用。

字符串字面量

你可以将字符串字面量赋值给 prop.

1
2
3
<MyComponent message="hello world" />
// 等价
<MyComponent message={'hello world'} />

当你将字符串字面量赋值给 prop 时,它的值是未转义的。

1
2
3
<MyComponent message="<3" />
// 等价
<MyComponent message={'<3'} />

Props 默认值为 “True”

如果你没给 prop 赋值,它的默认值是 true。

1
2
3
<MyTextBox autocomplete />
// 等价
<MyTextBox autocomplete={true} />

通常,我们不建议不传递 value 给 prop,因为这可能与 ES6 对象简写混淆,{foo} 是 {foo: foo} 的简写,而不是 {foo: true}。

属性展开

如果你已经有了一个 props 对象,你可以使用展开运算符 … 来在 JSX 中传递整个 props 对象。

1
2
3
4
5
6
7
8
function App1() {
  return <Greeting firstName="Ben" lastName="Hector" />;
}
// 等价
function App2() {
  const props = {firstName: 'Ben', lastName: 'Hector'};
  return <Greeting {...props} />;
}

你还可以选择只保留当前组件需要接收的 props,并使用展开运算符将其他 props 传递下去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Button = props => {
  const { kind, ...other } = props;
  const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
  return <button className={className} {...other} />;
};

const App = () => {
  return (
    <div>
      <Button kind="primary" onClick={() => console.log("clicked!")}>
        Hello World!
      </Button>
    </div>
  );
};

在上述例子中,kind 的 prop 会被安全的保留,它将不会被传递给 DOM 中的 <button> 元素。 所有其他的 props 会通过 …other 对象传递,使得这个组件的应用可以非常灵活。你可以看到它传递了一个 onClick 和 children 属性。

属性展开在某些情况下很有用,但是也很容易将不必要的 props 传递给不相关的组件,或者将无效的 HTML 属性传递给 DOM。我们建议谨慎的使用该语法。

JSX 中的子元素

包含在开始和结束标签之间的 JSX 表达式内容将作为特定属性 props.children 传递给外层组件。

字符串字面量

你可以将字符串放在开始和结束标签之间,此时 props.children 就只是该字符串。

1
<MyComponent>Hello world!</MyComponent>

这是一个合法的 JSX,MyComponent 中的 props.children 是一个简单的未转义字符串 “Hello world!"。

JSX 会移除行首尾的空格以及空行。与标签相邻的空行均会被删除,文本字符串之间的新行会被压缩为一个空格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div>Hello World</div>

<div>
  Hello World
</div>

<div>
  Hello
  World
</div>

<div>

  Hello World
</div>

JSX 子元素

子元素允许由多个 JSX 元素组成。

1
2
3
4
<MyContainer>
  <MyFirstComponent />
  <MySecondComponent />
</MyContainer>

React 组件也能够返回存储在数组中的一组元素:

1
2
3
4
5
6
7
8
9
render() {
  // 不需要用额外的元素包裹列表元素!
  return [
    // 不要忘记设置 key :)
    <li key="A">First item</li>,
    <li key="B">Second item</li>,
    <li key="C">Third item</li>,
  ];
}

JavaScript 表达式作为子元素

1
2
3
<MyComponent>foo</MyComponent>
// 等价
<MyComponent>{'foo'}</MyComponent>

函数作为子元素

通常,JSX 中的 JavaScript 表达式将会被计算为字符串、React 元素或者是列表。不过,props.children 和其他 prop 一样,它可以传递任意类型的数据,而不仅仅是 React 已知的可渲染类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 调用子元素回调 numTimes 次,来重复生成组件
function Repeat(props) {
  let items = [];
  for (let i = 0; i < props.numTimes; i++) {
    items.push(props.children(i));
  }
  return <div>{items}</div>;
}

function ListOfTenThings() {
  return (
    <Repeat numTimes={10}>
    // 回调函数作为children传递
      {(index) => <div key={index}>This is item {index} in the list</div>}
    </Repeat>
  );
}

布尔类型、Null 以及 Undefined 将会忽略

false, null, undefined, and true 是合法的子元素。但它们并不会被渲染。

1
2
3
4
5
6
7
8
9
10
11
<div />

<div></div>

<div>{false}</div>

<div>{null}</div>

<div>{undefined}</div>

<div>{true}</div>

值得注意的是有一些 “falsy” 值,如数字 0,仍然会被 React 渲染。例如,以下代码并不会像你预期那样工作,因为当 props.messages 是空数组时,0 仍然会被渲染

1
2
3
4
5
6
<div>
  {props.messages.length &&
    <MessageList messages={props.messages} />
  }
</div>
// 当messages是空数组,会在页面渲染出一个0

要解决这个问题,确保 && 之前的表达式总是布尔值:

1
2
3
4
5
<div>
  {props.messages.length > 0 &&
    <MessageList messages={props.messages} />
  }
</div>

反之,如果你想渲染 false、true、null、undefined 等值,你需要先将它们转换为字符串:

1
2
3
<div>
  My JavaScript variable is {String(myVariable)}.
</div>

shouldComponentUpdate

shouldComponentUpdate 的作用

alt text

节点 C2 的 shouldComponentUpdate 返回了 false,React 因而不会去渲染 C2,也因此 C4 和 C5 的 shouldComponentUpdate 不会被调用到。

对于 C1 和 C3,shouldComponentUpdate 返回了 true,所以 React 需要继续向下查询子节点。这里 C6 的 shouldComponentUpdate 返回了 true,同时由于渲染的元素与之前的不同使得 React 更新了该 DOM。

最后一个有趣的例子是 C8。React 需要渲染这个组件,但是由于其返回的 React 元素和之前渲染的相同,所以不需要更新 DOM。

显而易见,你看到 React 只改变了 C6 的 DOM。对于 C8,通过对比了渲染的 React 元素跳过了渲染。而对于 C2 的子节点和 C7,由于 shouldComponentUpdate 使得 render 并没有被调用。因此它们也不需要对比元素了。

Example

如果你的组件只有当 props.color 或者 state.count 的值改变才需要更新时,你可以使用 shouldComponentUpdate 来进行检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

你只要继承 React.PureComponent就行了。所以这段代码可以改成以下这种更简洁的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

大部分情况下,你可以使用 React.PureComponent 来代替手写 shouldComponentUpdate。但它只进行浅比较,所以当 props 或者 state 某种程度是可变的话,浅比较会有遗漏,那你就不能使用它了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class ListOfWords extends React.PureComponent {
  render() {
    return <div>{this.props.words.join(',')}</div>;
  }
}

class WordAdder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      words: ['marklar']
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 这部分代码很糟,而且还有 bug
    const words = this.state.words;
    words.push('marklar');
    this.setState({words: words});
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick} />
        <ListOfWords words={this.state.words} />
      </div>
    );
  }
}

问题在于 PureComponent 仅仅会对新老 this.props.words 的值进行简单的对比。由于代码中 WordAdder 的 handleClick 方法改变了同一个 words 数组,使得新老 this.props.words 比较的其实还是同一个数组。即便实际上数组中的单词已经变了,但是比较结果是相同的。可以看到,即便多了新的单词需要被渲染, ListOfWords 却并没有被更新。

不可变数据的力量

避免该问题最简单的方式是避免更改你正用于 props 或 state 的值。

1
2
3
4
5
6
7
8
9
10
11
handleClick() {
  this.setState(state => ({
    words: state.words.concat(['marklar'])
  }));
}
// 或者使用ES6的扩展运算
handleClick() {
  this.setState(state => ({
    words: [...state.words, 'marklar'],
  }));
};

LinkMap

前言

苹果对于提交审核的App中可执行文件有明确的大小限制,查看应用的可执行文件大小就需要利用LinkMap文件。

1.如何生成LinkMap文件

1).在工程的Build Setting中找到Write Link Map File并打开开关

2).在debug或者release下编译工程

3).先找到编译后的app文件,向上两级的Products同目录下有Intermediates.noindex文件夹,在该文件夹下能够找到名称为项目名-LinkMap-normal-x86_64.txt的文件(x86_64是架构,如果是真机,可能为arm64或者其他)

2.LinkMap有什么作用

LinKMap文件分为四个部分:

1:# Path:当前LinkMap编译的路径

1
# Path: /Users/zhoumo1/Library/Developer/Xcode/DerivedData/LinkMapTest-brvtnqhfmtphzohebrymhqgdzhwp/Build/Products/Debug-iphonesimulator/LinkMapTest.app/LinkMapTest

2:# Arch:当前编译的系统架构

1
# Arch: x86_64

3:# Object files:编译的所有文件

1
2
3
4
5
6
7
8
9
# Object files:
[  0] linker synthesized
[  1] /Users/zhoumo1/Library/Developer/Xcode/DerivedData/LinkMapTest-brvtnqhfmtphzohebrymhqgdzhwp/Build/Intermediates.noindex/LinkMapTest.build/Debug-iphonesimulator/LinkMapTest.build/LinkMapTest.app.xcent
[  2] /Users/zhoumo1/Library/Developer/Xcode/DerivedData/LinkMapTest-brvtnqhfmtphzohebrymhqgdzhwp/Build/Intermediates.noindex/LinkMapTest.build/Debug-iphonesimulator/LinkMapTest.build/Objects-normal/x86_64/ViewController.o
[  3] /Users/zhoumo1/Library/Developer/Xcode/DerivedData/LinkMapTest-brvtnqhfmtphzohebrymhqgdzhwp/Build/Intermediates.noindex/LinkMapTest.build/Debug-iphonesimulator/LinkMapTest.build/Objects-normal/x86_64/main.o
[  4] /Users/zhoumo1/Library/Developer/Xcode/DerivedData/LinkMapTest-brvtnqhfmtphzohebrymhqgdzhwp/Build/Intermediates.noindex/LinkMapTest.build/Debug-iphonesimulator/LinkMapTest.build/Objects-normal/x86_64/AppDelegate.o
[  5] /Users/zhoumo1/Downloads/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.2.sdk/System/Library/Frameworks//Foundation.framework/Foundation.tbd
[  6] /Users/zhoumo1/Downloads/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.2.sdk/usr/lib/libobjc.tbd
[  7] /Users/zhoumo1/Downloads/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.2.sdk/System/Library/Frameworks//UIKit.framework/UIKit.tbd

4:# Sections:

段表标题中列举出了地址,大小,段类型以及段名称。
数据类型分为两种不同类型,一种是TEXT,用于保存程序代码段经过编译后的机器码,另外一种是DATA用于保存诸如全局变量或者局部变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Sections:
# Address Size        Segment Section
0x100001590   0x00000453  __TEXT  __text
0x1000019E4   0x00000036  __TEXT  __stubs
0x100001A1C   0x0000006A  __TEXT  __stub_helper
0x100001A86   0x00000A70  __TEXT  __objc_methname
0x1000024F6   0x0000003C  __TEXT  __objc_classname
0x100002532   0x00000878  __TEXT  __objc_methtype
0x100002DAA   0x0000007A  __TEXT  __cstring
0x100002E24   0x00000186  __TEXT  __entitlements
0x100002FAC   0x00000048  __TEXT  __unwind_info
0x100003000   0x00000010  __DATA  __nl_symbol_ptr
0x100003010   0x00000048  __DATA  __la_symbol_ptr
0x100003058   0x00000010  __DATA  __objc_classlist
0x100003068   0x00000010  __DATA  __objc_protolist
0x100003078   0x00000008  __DATA  __objc_imageinfo
0x100003080   0x00000C48  __DATA  __objc_const
0x100003CC8   0x00000030  __DATA  __objc_selrefs
0x100003CF8   0x00000008  __DATA  __objc_classrefs
0x100003D00   0x00000008  __DATA  __objc_superrefs
0x100003D08   0x00000008  __DATA  __objc_ivar
0x100003D10   0x000000A0  __DATA  __objc_data
0x100003DB0   0x000000C0  __DATA  __data

5:# Symbols:文件中每一个方法的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Symbols:
# Address Size        File  Name
0x100001590   0x00000040  [  2] -[ViewController viewDidLoad]
0x1000015D0   0x00000050  [  2] -[ViewController viewDidAppear:]
0x100001620   0x00000050  [  2] -[ViewController viewWillAppear:]
0x100001670   0x00000050  [  2] -[ViewController viewDidDisappear:]
0x1000016C0   0x0000003C  [  2] -[ViewController didReceiveMemoryWarning]
0x100001700   0x00000090  [  3] _main
0x100001790   0x00000080  [  4] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100001810   0x00000040  [  4] -[AppDelegate applicationWillResignActive:]
0x100001850   0x00000040  [  4] -[AppDelegate applicationDidEnterBackground:]
0x100001890   0x00000040  [  4] -[AppDelegate applicationWillEnterForeground:]
0x1000018D0   0x00000040  [  4] -[AppDelegate applicationDidBecomeActive:]
0x100001910   0x00000040  [  4] -[AppDelegate applicationWillTerminate:]
0x100001950   0x00000020  [  4] -[AppDelegate window]
0x100001970   0x00000040  [  4] -[AppDelegate setWindow:]
0x1000019B0   0x00000033  [  4] -[AppDelegate .cxx_destruct]
···

3.如何减少库大小

除了第三方的库以外,我们还能对自己的代码做些什么?

1)注释或删除空方法和没有调用的方法

如上文分析,类中存在的方法就会被计算大小,无论是否调用。所以可以删除类中 如:

1
2
3
4
5
6
- (void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
}
- (void)viewDidDisappear:(BOOL)animated{
    [super viewDidDisappear:animated];
}

多加了两个空方法的大小对比如下,虽然空方法占用的很少,但是如果数量比较多还是很可观。

2)比较去重

分析过滤项目中的重复代码。通过整合减少代码量。

介绍一种去重的方法,使用PMD去重:
1.下载:https://sourceforge.net/projects/pmd/files/pmd/
2.cd 到bin目录中
3.在终端输入

1
./run.sh cpd --language ObjectiveC --minimum-tokens 100 --files /Users/zhoumo1/Desktop/LinkMap/LinkMapTest/LinkMapTest > /Users/zhoumo1/Desktop/check.txt 

4.查看结果~完美

3)升级最低版本||重构

这没什么好说的,iOS9已经支持到500M,如果实在不能删减,升级最低支持版本是最有效的。

重构风险较大,并且重构后也不一定会小很多,不支持。

4.LinkMap比对

为了方便查看做了以上减少工作后的结果如何,于是写了一个简单的Mac应用,主要功能是对比两份LinkMap文件。

git地址:https://github.com/zhoumo199163/compareLinkMap

如果觉得对你有用,欢迎star~

算法和数据结构

1.找到字符串中最长无重复子串

  • 例如aabbc,最长无重复子串abc,长度3;abcd,长度4;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 思路:
// 1.先申请一个字典,key是每一个字符,value是字符上一次出现的位置
// 2.pre:该字符上次出现的位置
// 3.cur:当前位置和上次出现位置的距离
// 4.len:最长距离
- (void)maxUniqu:(NSString *)string{
    NSMutableDictionary *nums = [NSMutableDictionary new];
    NSArray  *array = [string componentsSeparatedByString:@","];
    [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        nums[obj] = @(-1);
    }];
    
   __block int pre = -1;
   __block int len = 0;
   __block int cur = 0;
    [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        pre = MAX(pre, [nums[obj] intValue]);
        cur = idx - pre;
        len = MAX(len, cur);
        nums[obj] = @(idx);
        NSLog(@"\nobj:%@\npre:%d\ncur:%d\nlen:%d\nnums:%@",obj,pre,cur,len,nums);
    }];
}

iOS_Q&A

Q1.通知是同步还是异步?

  • A1:通知是同步的。测试如下:
 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(print:) name:@"GCDNotification" object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(print1:) name:@"GCDNotification" object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(print2:) name:@"GCDNotification" object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(print3:) name:@"GCDNotification" object:nil];

    [[NSNotificationCenter defaultCenter] postNotificationName:@"GCDNotification" object:nil];
        NSLog(@"发出通知----%@",[NSThread currentThread]);

    - (void)print:(NSNotification *)notif{
        NSLog(@"0收到通知----%@",[NSThread currentThread]);

        sleep(10);
}
// log 
2017-12-24 15:07:58.552422+0800 KVOTest[7306:368524] 0收到通知----<NSThread: 0x604000066840>{number = 1, name = main}
2017-12-24 15:08:08.553940+0800 KVOTest[7306:368524] 1收到通知----<NSThread: 0x604000066840>{number = 1, name = main}
2017-12-24 15:08:18.555010+0800 KVOTest[7306:368524] 2收到通知----<NSThread: 0x604000066840>{number = 1, name = main}
2017-12-24 15:08:28.557206+0800 KVOTest[7306:368524] 3收到通知----<NSThread: 0x604000066840>{number = 1, name = main}
2017-12-24 15:08:38.559301+0800 KVOTest[7306:368524] 发出通知----<NSThread: 0x604000066840>{number = 1, name = main}
  • 模拟实现异步接收的效果如下:
1
2
3
4
5
6
7
8
9
10
11
12
 -(void)print:(NSNotification *)notif{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"0收到通知----%@",[NSThread currentThread]);
        sleep(10);
    });
}
// log
2017-12-24 15:11:09.680454+0800 KVOTest[7325:373178] 2收到通知----<NSThread: 0x60000026c880>{number = 5, name = (null)}
2017-12-24 15:11:09.680454+0800 KVOTest[7325:373056] 3收到通知----<NSThread: 0x60400006e800>{number = 1, name = main}
2017-12-24 15:11:09.680488+0800 KVOTest[7325:373176] 0收到通知----<NSThread: 0x60000026c040>{number = 3, name = (null)}
2017-12-24 15:11:09.680491+0800 KVOTest[7325:373175] 1收到通知----<NSThread: 0x60400027d440>{number = 4, name = (null)}
2017-12-24 15:11:19.682627+0800 KVOTest[7325:373056] 发出通知----<NSThread: 0x60400006e800>{number = 1, name = main}

Q2.UIView和CALayer的父类及关系?

  • UIView的父类是UIResponse,可以响应事件和手势
  • CALyer的父类是NSObject,不可以响应事件和手势,更偏重内容绘制
  • 每个UIView都有自己的CALayer,UIView的frame只是返回CALayer的frame
  • 在 View显示的时候,UIView 做为 Layer 的 CALayerDelegate

Q3.手动设置UIView的bounds会怎样?

  • [UIview setBounds:]改变当前view相对于子view的坐标原点。
 UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(0,0, 375, 100)];
    [view1 setBackgroundColor:[UIColor grayColor]];
    [self.view addSubview:view1];

    // 改变后,view1的原点在(0,-100)位置
    self.view.bounds = CGRectMake(0, -100, CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame));

Q4.在xib中创建一个全view大小的tableview,会不会被导航栏遮挡?为什么?

  • 正常情况下,导航栏的透明度可影响视图是否被遮挡
 // 设置不透明,self.view在导航栏下面展示,不遮挡
 // 否则,self.view为全屏展示,会被导航栏遮挡
 self.navigationController.navigationBar.translucent = NO;
  • 在不考虑以上透明度的影响,tableview不被遮挡的同时还需要滑动时在不透明的导航栏中透出
  // iOS10 以上系统自动设置,以下需要手动设置
  [self.tableView setContentInset:UIEdgeInsetsMake(64, 0, 0, 0)];

Q5.动态库和静态库的区别?系统库属于哪种?

  • 静态库以.a和.framework作为文件后缀名,在使用时会被完全的copy一份到文件中,多次使用就包含多次拷贝文件,平时使用的第三方库大部分都是静态库,会使app的体积变大。
  • 动态库以.framework和.tbd(之前叫.dylib)作为文件后缀名,在使用时不会拷贝,由系统动态加载到内存中,只加载一次,多次使用可节省空间。如系统库UIKit等都是动态库。
  • 动态库制作可以包含其他静态库(.a/.framework)
  • 静态库制作可以包含其他静态库(.a/.framework)

Q6.为什么加载xib文件比纯代码耗时?

  • xib实质是xml文件,系统先要解析xml文件中的view及参数属性。

Q7.podfile.lock的作用?

  • 在使用命令【pod install】以后会生成podfile.lock文件,这个文件中保存已经安装的Pods依赖库的版本。
  • 在多人协作时,特别是在podfile文件中没有指定依赖版本号时,如果有podfile.lock文件,其他人在执行【pod install】时就会和最开始的用户保持版本一致。如果没有podfile.lock文件,可能更新出不同的版本。所以podfile.lock需要加入版本管理。
 // podfile 
 platform :ios, '9.0'
def shared_pods
    pod 'FMDB' // 不指定版本,获取最新版本
end
target 'LittleNotes' do 
    shared_pods
end
target 'LittleNotesWidget' do
    shared_pods
end
 //podfile.lock
 PODS:
  - FMDB (2.7.2):
    - FMDB/standard (= 2.7.2)
  - FMDB/standard (2.7.2)
DEPENDENCIES:
  - FMDB
SPEC CHECKSUMS:
  FMDB: 6198a90e7b6900cfc046e6bc0ef6ebb7be9236aa
PODFILE CHECKSUM: f5e7b7c51ca371b02aa28c158108e5f9ccdf3033
COCOAPODS: 1.3.1

Q8.如何优化应用启动时间?

  • 冷启动:启动的应用不在后台运行,系统需要重新创建一个新进程分配给应用。App启动时间是从手指点击到调用applicationWillFinishLaunching结束
  • 热启动:应用已经运行,但是被后台挂起,比如按了home键。App的启动时间是从手指点击到调用applicationWillEnterForeground。
  • 测量应用启动时间:Xcode - Edit scheme - Auguments 配置环境变量DYLD_PRINT_STATISTICS为1.
 Total pre-main time:  99.69 milliseconds (100.0%)
         dylib loading time:  39.31 milliseconds (39.4%)
        rebase/binding time:   7.57 milliseconds (7.5%)
            ObjC setup time:  10.25 milliseconds (10.2%)
           initializer time:  42.39 milliseconds (42.5%)
           slowest intializers :
             libSystem.B.dylib :  11.93 milliseconds (11.9%)
   libBacktraceRecording.dylib :   7.14 milliseconds (7.1%)
    libMainThreadChecker.dylib :  17.24 milliseconds (17.2%)

影响启动时间因素: - 动态库加载时间。一般应用会加载 100 到 400 个 dylib 文件,但大部分都是系统 dylib,它们会被预先计算和缓存起来,加载速度很快。但是另外集成的动态库应该尽量减少动态库的数量,或者将多个动态库合成一个。尽量保证将App现有的非系统级的动态库个数保证在6个以内。 - 指针修正时间。减少指针数量。就是减少App中的类、category、selector的数量。 - 初始化时间:使用+initialize代替+load方法,不要使用attribute((constructor)) 将方法显式标记为初始化器

  load -> attribute((constructor)) -> main -> initialize
  + (void)initialize{
    // 在类被第一次使用的时候调用
  }
  + (void)load{
    // 在类被运行时加载时调用
  }
  __attribute((constructor)) void beforeMain(){
  //是GCC的扩展语法(黑魔法),由它修饰过的函数,会在main函数之前调用
  }

Q9.dSYM是什么文件,有什么作用?

  • 每次编译build/Archive App时,会生成一个同名的dSYM文件,dSYM 是保存 16 进制函数地址映射信息的中转文件.
  • .dSYM中真正保存符号表数据的是DWARF文件。DWARF(DebuggingWith Arbitrary Record Formats),是ELF和Mach-O等文件格式中用来存储和处理调试信息的标准格式。
  • .dSYM文件主要用于在发生崩溃时,根据崩溃内存地址查找到具体的崩溃位置。

  • 1.首先必须保.dSYM和.app文件是对应的

 // 通过读取文件中的UUID
 dwarfdump --uuid xx.app/xx (xx代表你的项目名)
 dwarfdump --uuid xx.app.dSYM;
  • 2.崩溃错误信息
1
2
3
4
5
 // 一般崩溃信息格式
appName 0x000000010011496c appName + 379244

 // 0x000000010011496c:崩溃内存地址,十六进制
 379244:地址偏移量,十进制
  • 3.定位
1
2
3
4
5
6
7
8
9
10
11
12
 // 终端运行
 atos -arch arm64(1) -o "dSYM文件路径(2)" -l 0x00000001000B8000(3) 0x000000010011496c
 
 // 1.处理器指令集,真机64位处理器为arm64,32位处理器为armv7或armv7s
 // 2.将dSYM文件-右键-显示包内容,定位到DWARF目录下的app同名文件,如:/Users/zm/Desktop/ywh_dSYM/2.1.1/uclture.app.dSYM/Contents/Resources/DWARF/玩事
 // 3.初始内存地址:将十六进制崩溃内存地址转化为十进制后,减去地址偏移量,获得十进制内存初始地址,再转化成十六进制
 如:0x000000010011496c(16)= 4296100204(10)
 初始地址(10) = 4296100204 - 379244 = 4295720960
 4295720960(10) = 1000B8000(16)
 
 // 运行结果
 -[HonorChatViewController tableView:heightForRowAtIndexPath:] (in ) (HonorChatViewController.m:193)

Q9.关于设置frame和autolayout布局的区别

  • 手动设置frame,虽然也可以设置相对位置,不过需要大量的计算。
  • 设置autolayout不需要自己去计算
  • 直接设置frame在屏幕旋转等事件发生时需要重新适配计算位置,autolayout不需要

Q10.怎么避免NSTimer的循环引用?

  • 1.不重复timer
1
2
  _timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(todoSomething) userInfo:nil repeats:NO];
  // timer在执行一次selector后自动调用invalidate方法,不会造成循环应用
  • 2.重复执行的timer
1
2
 _timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(todoSomething) userInfo:nil repeats:YES];
 // 重复的timer必须手动执行invalidate方法,且不能再dealloc中执行,因为timer强引用self,导致self不能释放,不会触发dealloc
    1. invalidate方法的作用
1
2
3
4
5
6
  Stops the timer from ever firing again and requests its removal from its run loop.
This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.
停止计时器再次触发,并请求将其从运行循环中删除。
该方法是从NSRunLoop对象中删除计时器的惟一方法。NSRunLoop对象删除了它对计时器的强引用,或者是在无效的方法返回之前,或者在稍后的某个时间点。
如果它配置了目标和用户信息对象,接收者也会删除对这些对象的强引用。

Q11.在block中使用_xxx的方式使用属性会不会循环引用?

  • 会产生循环引用
  • _name和self.name的区别是self.name直接调用属性的get方法,
  • _name是调用类的实例变量。都会传入self被block强引用。
1
2
3
4
self.block = ^(id target) {
        _name = @"weekend";
    };
    self.block(self);

Q12.设计一个方法查询指定view上所有相同类型的子控件

  • 1.需要想到view的子view的子view…中也要查找
  • 2.自定义继承的控件也需要查找
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1.使用递归查找子view
// 2.自定义控件使用isKindOfClass
- (NSInteger)searchSubviewsCountByType:(NSString *)type atView:(UIView *)view{
   __block NSInteger count = 0;
    NSArray <UIView*> *subviews = view.subviews;
    [subviews enumerateObjectsUsingBlock:^(UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSInteger subCount = 0;
        if(obj.subviews.count != 0){
            subCount = subCount + [self searchSubviewsCountByType:type atView:obj];
        }else{
            Class targetClass = NSClassFromString(type);
            if([obj isKindOfClass:targetClass]){
                subCount++;
            }
        }
        count = count + subCount;
    }];
    return count;
}

// isKindOfClass:返回一个布尔值,该值表示接收者是给定类的实例还是继承该类的任何类的实例。
Returns a Boolean value that indicates whether the receiver is an instance of given class or an instance of any class that inherits from that class.
// isMemberOfClass:返回一个布尔值,该值表示接收者是否为给定类的实例。
Returns a Boolean value that indicates whether the receiver is an instance of a given class.

Q13.在主线程中新建一个串行队列,同步执行任务,任务在哪个线程中执行?为什么?

  • 任务执行在主线程
  • 串行队列:队列中的任务按照顺序执行
  • 并行队列:队列中的任务同时执行
  • 同步:阻塞当前线程,直到任务执行完毕
  • 异步:不阻塞当前线程
1
2
3
4
5
6
7
8
9
10
11
// 同步串行:阻塞当前主线程,并顺序执行。不同的任务队列不会发生死锁。
 dispatch_sync(dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL), ^{
        NSLog(@"currentThread:%@",[NSThread currentThread]);
    });
//log:
currentThread:<NSThread: 0x604000076c40>{number = 1, name = main}

// 死锁:当前队列等待block中的任务执行完毕,block中任务阻塞主线程等待主线程队列中的任务执行完毕。
dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"currentThread:%@",[NSThread currentThread]);
    });

Q14.在dealloc中写以下方法会输出什么?

1
2
3
4
- (void)dealloc{
    __weak typeof(self) weakSelf = self;
    NSLog(@"weakSelf:%@",weakSelf);
}
  • 运行崩溃:objc[1653]: Cannot form weak reference to instance (0x7fc3a8c0c920) of class TestViewController. It is possible that this object was over-released, or is in the process of deallocation.
  • dealloc:释放被接收方占用的内存。 接收到接收方的后续消息可能会产生一个错误,指示消息被发送到一个回收对象(前提是该回收的内存还没有被重用)。
  • 当前self在回收过程中,不允许弱引用

Q15.怎么在当前页面关闭的时候取消还没有下载完成的任务?

  • 1.NSOperation
 // 1.添加任务到队列
  self.operationQueue = [NSOperationQueue new];
    self.operationQueue.maxConcurrentOperationCount = 1;
    for(int i = 0;i<10;i++){
        [self.operationQueue addOperationWithBlock:^{
            sleep(1);
            NSLog(@"i == %d",i);
        }];
    }
  // 2. 挂起队列,不能暂停正在执行的任务
   [self.operationQueue setSuspended:YES];
  // 3. 取消挂起
  [self.operationQueue setSuspended:No];
  // 4.取消全部任务
    [self.operationQueue cancelAllOperations];
   // 5.取消单个任务
    NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{
        // do something
    }];
    [self.operationQueue addOperation:blockOp];
    [blockOp cancel];
  • 2.GCD
 // 1.添加任务到队列
  dispatch_block_t gcdBlcok = dispatch_block_create(0, ^{
        NSLog(@"to do something");
    });
    dispatch_async(self.queue, gcdBlcok);
  // 2.取消任务
  dispatch_block_cancel(gcdBlcok);
  OR dispatch_cancel(gcdBlcok);
  // 3.挂起队列
  dispatch_suspend(self.queue);
  // 4.取消挂起
  dispatch_resume(self.queue);
  • 3.页面关闭时,取消队列中还没有开始的任务。如果使用GCD需要将任务单独生成一个dispatch_block_t对象,如果是NSOperation,直接调用 cancelAllOperations。

Q16 objectForKey和valueForKey的区别

首先, objectForKey:是一个NSDictionary方法,而valueForKey:是任何KVC投诉类所需的KVC协议方法 - 包括NSDictionary。

文档提示NSDictionary实现其valueForKey:方法使用其objectForKey:实现。 换句话说- [NSDictionary valueForKey:]调用[NSDictionary objectForKey:]

1
2
3
/* Return the result of sending -objectForKey: to the receiver.
*/
- (nullable ObjectType)valueForKey:(NSString *)key;
  • KVC协议仅适用于NSString *键,因此 valueForKey:只接受NSString * (或子类)作为键,使用别的类型会崩溃;而objectForKey:接受任何可复制(符合NSCopying协议)对象作为密钥。

  • 如果valueForKey的key的第一个字符是@,@会被去掉

  • valueForKey找不到value也不会崩溃
1
2
3
4
5
6
7
8
9
10
11
12
13
NSDictionary *dict = @{@"@theKey":@"@theValue"[NSNumber numberWithInt:1]:@[],@"theKey":@"theValue"};

NSString *value1 = [dict objectForKey:@"@theKey”]; 
//value1=@theValue

NSString *value2 = [dict valueForKey:@"@theKey”]; 
// value2=nil,虽然@被去掉了却无法取到theValue
    
NSArray *value3 = [dict objectForKey:[NSNumber numberWithInt:1]];  
// value3 = @[]

NSArray *value4 =[dict valueForKey:@"1”]; 
// value4 = nil 如果这里的key是[NSNumber numberWithInt:1],会报警告并且崩溃