一道面试题里藏的IOS世界

上边这张图是前段时间网上比较火的一套面试题中的。这套题来源是 目前就职于百度的孙源。

前序

会有开发者,看这道题目时会觉着没有啥太大毛病,实际中他们也可能就是这样写的。

其实在github上会经常看到这样类似的代码,特别是在一些小demo里很常见。之所以忽略其中的问题,主要是因为没有认识到规范的写法,以及这种写法可能引起的问题。扯远点就是,一个人的编码语言认知水平会极大的影响他的编码行为。然而认知水平和学习的途径有莫大的关系。

前几个月,我们公司招聘移动开人员,本人也面试不少android和ios的开发者,发现一个很普遍的问题:代码不规范。不论是刚从培训学校里刚出来的,还是已经5~6年经验的开发者。当提到代码的规范时,大多回答的就是驼峰式命名法,甚至有位”资深的”求职者向我”普及现今业界的普遍形式”:其实代码规范与否并不重要,对多数中小创业公司而言,重要的是快速上线,及时迭代,占有市场。话是不错,也道出了像我们这种在风口创业公司的基本路线,就是及早入场,拿到牌照,占有市场。但最终我也没有通过他的面试。理由是:工作经验丰富(开发过Symbian,android,iOS)但不适合做开发(因android\iOS不资深。我们要招聘资深的android,懂点iOS,创业公司嘛,iOS和android分的不是很清,是活都干)。

上边扯远了,代码的规范与否,最初的学习途径和形式固然重要,更重要的是在后期的工作中,是否较为全面系统的从官方提供的资料中进行相关学习。可能有人觉得不重要,但是在我看来,这是一名合格开发者的必经之路。你所用的语言、规则,都是人家定的,即便能开发出可用的app,那也未必合格,特别是项目逐渐做大之后,简单的模式,不规范的代码所带来的隐患就会逐渐呈现出来。

下边首先会解析问题,并就部分内容深入解析,如有疑问还请留言相告

解析

1、枚举

问题点:

1、题目中写枚举是使用C的风格; 2、枚举定义的不规范; 3、枚举的内容不严谨。

1.1 定义规范的枚举

官方文档中,enum 建议使用 NS_ENUM 和 NS_OPTIONS 宏来定义枚举类型。 NS_ENUM枚举项的值为NSInteger,定义通用的枚举; NS_OPTIONS枚举项的值为NSUInteger,定义位移枚举。 一般开发者可能用不到后者,但如果进入到UIKit的头文件里,就会看到很多这种书写格式。

enum 中驼峰命名法和下划线命名法的使用:枚举类型的命名规则和函数的命名规则相同:命名时使用驼峰命名法,勿同时使用下划线命名法。

1.2 定义规范的枚举内容

试题的model实际使用场景不清楚,这里将该枚举场景放宽到生活中: 那么除了男性,女性,实际上还应该有包括:内心男性的女性,以及内心女性的男性,以及无性,以及其他未知的性别(考虑未知的特殊情况。当然下边的枚举针对不同的行业场景,可能要进行调整)

1.3 枚举的深入

这两个宏的定义在Foundation.framework的NSObjCRuntime.h中:

#if (__cplusplus && __cplusplus >= 201103L && (__has_extension(cxx_strong_enums) || __has_feature(objc_fixed_enum))) || (!__cplusplus && __has_feature(objc_fixed_enum))  
#define NS_ENUM(_type, _name) enum _name : _type _name; enum _name : _type  
#if (__cplusplus)  
#define NS_OPTIONS(_type, _name) _type _name; enum : _type  
#else  
#define NS_OPTIONS(_type, _name) enum _name : _type _name; enum _name : _type  
#endif  
#else  
#define NS_ENUM(_type, _name) _type _name; enum  
#define NS_OPTIONS(_type, _name) _type _name; enum  
#endif  

其实上述的写法,等价于:

typedef enum JUserGender : NSInteger JUserGender;  
enum JUserGender : NSInteger {  }

从枚举定义来看,NS_ENUM和NS_OPTIONS本质是一样的,仅仅从字面上来区分其用途。NS_ENUM是通用情况,NS_OPTIONS一般用来定义具有位移或特点的情况。

1.4 优化后的写法

typedef NS_ENUM(NSInteger, JUserGender) {
    JUserGenderUnknown,
    JUserGenderMale,
    JUserGenderGay,
    JUserGenderFemale,
    JUserGenderLesbian,
    JUserGenderNeuter
};

2、属性

** @property本质是: @property = ivar + getter + setter;**

也就是说,属性=ivar(实例变量)+ 存取方法。它作为 OC的一项特性,主要的作用就在于封装对象中的数据。 OC 对象通常会把其所需要的数据保存为各种实例变量。

属性的修饰符

原子性 — nonatomic 特质 (不声明nonatomic,编译器就默认使用同步锁保证其原子性atomic,从而保证其线程安全。但如果真的是多个线程同时操作该属性,最好采用其它手段保证属性thread safety)

读/写权限 — readwrite(读写)、readonly (只读)

内存管理语义—assign、strong、 weak、unsafe_unretained、copy

方法名—getter= 、setter=

下边分别解说

2.1、age属性 类型的选择

age 属性的类型:应避免使用基本类型,建议使用 Foundation 数据类型,对应关系如下:

int -> NSInteger
unsigned -> NSUInteger
float -> CGFloat 同时考虑到 age 的特点,以及出于32/64 bit的考虑应使用 NSUInteger。

2.2、属性的内存管理

详见官方文档 传送门

1、weak VS assign

使用weak的情形: 1、在 ARC 中,可能出现循环引用时,往往要通过weak来解决,比如: delegate代理属性 2、自身已经强引用,此时也会使用weak,比如:自定义IBOutlet控件属性一般使用 weak;(也可使用strong) .. 相同: weak义为“非拥有关系”,设置方法既不保留新值,也不释放旧值(此同assign), 然而对象销毁时,属性值也会清空(assign 的set只会执行针对CGFloat、NSlnteger等简单的赋值操作), weak必须用于OC对象(assigin可以用非OC对象)。

2、runtime中的weak原理: runtime中实现weak的原理是:runtime对注册的类进行布局,weak对象会放入一个hash表中。把weak指的对象内存地址作为key,当此对象的引用计数为0时dealloc,假如weak指向的对象内存地址是a,那么就会以a为键,在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil。

3、copy

3.1、copy复制出来是不可变的,所以可变属性不建议用copy:

@property (copy) NSMutableArray *array; 这里使用copy结果导致属性是不可变的。同时这里使用了atomic会影响性能

3.2、NSArray等经常使用copy关键字: 因为他们有对应的可变类型:NSMutableArray,他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。

3.3、block 也经常使用 copy 关键字 具体原因见官方文档:传送门 block 使用copy是从MRC遗留下来的“传统”,在MRC中,方法内部的block是在栈区的,使用copy 可以把它放到堆区。在 ARC中不强制。

3.4、copy修饰自定义类 如果自定义的对象分为可变与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。针对实际可能出现的复杂情况,实现主要代码如下:

// .h文件
   typedef NS_ENUM(NSInteger, JUserGender) {
       JUserGenderUnknown,
       JUserGenderMale,
       JUserGenderGay,
       JUserGenderFemale,
       JUserGenderLesbian,
       JUserGenderNeuter
   };

  @interface JUser : NSObject<NSCopying>

  @property (nonatomic, readonly, copy)      NSString    *name;
  @property (nonatomic, readonly, assign)    NSUInteger  age;
  @property (nonatomic, readwrite, assign)   JUserGender sex;

  - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(JUserGender)sex;
  + (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(JUserGender)sex;
- (void)addFriend:(JUserGender *)user;
- (void)removeFriend:(JUserGender *)user;
@end

// .m文件

@implementation JUser {
    NSMutableSet *_friends;
}

- (void)setName:(NSString *)name {
    _name = [name copy];
}

- (instancetype)initWithName:(NSString *)name
                         age:(NSUInteger)age
                         sex:(JUserGender)sex {
    if(self = [super init]) {
        _name = [name copy];
        _age = age;
        _sex = sex;
        _friends = [[NSMutableSet alloc] init];
    }
    return self;
}

- (void)addFriend:(JUserGender *)user {
    [_friends addObject:user];
}

- (void)removeFriend:(JUserGender *)user {
    [_friends removeObject:user];
}

- (id)copyWithZone:(NSZone *)zone {
    CYLUser *copy = [[[self class] allocWithZone:zone]
                     initWithName:_name
                     age:_age
                     sex:_sex];
    copy->_friends = [_friends mutableCopy];
    return copy;
}

- (id)deepCopy {
    JUser *copy = [[[self class] allocWithZone:zone]
                     initWithName:_name
                     age:_age
                     sex:_sex];
    copy->_friends = [[NSMutableSet alloc] initWithSet:_friends
                                             copyItems:YES];
    return copy;
}

@end

2.3、在@protocol中使用@property:

需要借助运行时: objc_setAssociatedObject objc_getAssociatedObject

2.5、属性相关的@synthesize和@dynamic

如果@synthesize和@dynamic都没写,那么默认的就是@syntheszie var = _var;

@synthesize:没有手动实现setter、getter,编译器会自动为你加上这两个方法。除此之外,可以在类的.m文件中,通过@synthesize指定实例变量的名字: @implementation Juser @synthesize name = _myName; @end 那么就使用了别名_myName来代替name

@dynamic:告诉编译器属性的setter与getter方法由用户自己实现,不自动生成。

2.2、属性内存管理-weak的使用

2.3、性别属性的设置

3、doLogIn方法使用不当:

doLogIn方法命名不规范,logIn已经可以表达登录的意思,这里加个前缀是多余的。

除此之外,最重要的是doLogIn登录方法的操作属于业务逻辑,从类名UserModel,以及属性的命名方式,该类应该是一个 Model。无论是MVC还是MVVM等模式,业务逻辑都不应当写在 Model里:MVC应写在C,MVVM应写在 VM。

4、类的初始化规范:

4.1、类的初始化规范:

Objective-C 初始化方法有designated initializer和 secondary initializer两种。基本的规范是:

1、并且一个类应该有且只有一个 designated 初始化方法,其他的初始化方法应该调用这个 designated 的初始化方法。 2、每个类的初始化过程应当是按照从子类到父类的顺序,依次调用Designated Initializer。并且用父类的Designated Initializer初始化一个子类对象,亦是如此。

4.2、方法命名的规范性:

-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age; 方法中连接两个参数,应该去掉with,直接上参数。参考苹果写法:

  • (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);

上边提到运行时,下边简单的说下运行时

5、运行时

5.1、研究工具Clang 参见后边的番外。

5.2、objc中向nil对象发送消息

objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。 objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,然后在发送消息的时候,objc_msgSend方法不会返回值,所谓的返回内容都是具体调用时执行的。so,向一个nil对象发送消息,首先在寻找对象的isa指针(0)时就返回了,所以不会出现什么问题。

5.3、[obj foo] VS objc_msgSend()

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        JTest *test = ((JTest *(*)(id, SEL))(void *)objc_msgSend)((id)((JTest *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("JTest"), sel_registerName("alloc")), sel_registerName("init"));

        ((id (*)(id, SEL, SEL))(void *)objc_msgSend)((id)test, sel_registerName("performSelector:"), (sel_registerName("sendSth")));

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_sj_cddw8y954pz07p2x3qcsd9640000gn_T_main_c62014_mi_0);
        return 0 ;
    }
}
/*.m文件在下方*/

简化下: ((void ()(id, SEL))(void )objc_msgSend)((id)obj, sel_registerName(“foo”)); 也就是说: [obj foo];在objc动态编译时,会被转意为:objc_msgSend(obj, @selector(foo))。 当调用该对象上某个方法,而该对象上没有实现这个方法的时候, 可以通过“消息转发”进行解决。如果,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。

5.4、 _objc_msgForward

_objc_msgForward是 IMP 类型,用于消息转发的,当向一个对象发送一条消息,但它并没有实现时,_objc_msgForward会尝试做消息转发。

5.4.1 功能:

在objc-runtime-new.mm里面搜索到如下的说明:

/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup. 
* initialize==NO tries to avoid +initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known. 
*   If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use 
*   must be converted to _objc_msgForward or _objc_msgForward_stret.
*   If you don't want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/

在“消息传递”过程中,objc_msgSend的动作比较清晰:首先在 Class 中的缓存查找 IMP (缓存没有时重建缓存),如果没找到,则向父类的 Class 查找。如果一直查找到根类仍旧没有实现,则用_objc_msgForward函数指针代替 IMP 。最后,执行这个 IMP 。

5.4.2 实战说明:

尝试向一个对象发送一条错误的消息,并查看一下_objc_msgForward是如何进行转发的。 随意想一个不存在的方法的类发一条消息,

int main(int argc, char * argv[]) {

      @autoreleasepool {
         JTest *test = [[JTest alloc] init];
         [test performSelector:(@selector(QQQQQQQQ))];
         NSLog(@"打印:3code.info");
         return 0 ;
     }
}

断点在发消息之处。运行至此,在此gdb中输入下面的命令:

call (void)instrumentObjcMessageSends(YES) 然后在终端执行

 $ open /private/tmp 找到最新的一条(msgSends-4488),打开如下:
+ JTest NSObject initialize
+ JTest NSObject alloc
- JTest NSObject init
- JTest NSObject performSelector:
+ JTest NSObject resolveInstanceMethod:
+ JTest NSObject resolveInstanceMethod:
- JTest NSObject forwardingTargetForSelector:
- JTest NSObject forwardingTargetForSelector:
- JTest NSObject methodSignatureForSelector:
- JTest NSObject methodSignatureForSelector:
- JTest NSObject class
- JTest NSObject doesNotRecognizeSelector:
- JTest NSObject doesNotRecognizeSelector:
- JTest NSObject class
- OS_xpc_serializer OS_object _xref_dispose
- OS_xpc_serializer OS_xpc_object _dispose
- OS_xpc_serializer NSObject dealloc
....

从中可以看出它做的几件主要的是:

  • 1、调用resolveInstanceMethod:方法 (resolveClassMethod:)。允许用户在此时为该 Class 动态添加实现。如果有实现了,则调用并返回YES,重新开始objc_msgSend流程(这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod)。如果仍没实现,继续下面的动作。
  • 2、调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非nil对象。否则返回nil继续下面的动作。此处不可返回self,否则会形成死循环。
  • 3、调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。
  • 4、调用forwardInvocation:方法,将第3步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了,并返回非ni。
  • 5、调用doesNotRecognizeSelector: 默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。

上面前4个方法均是模板方法,开发者可以overwrite,由runtime来调用。最常见的实现消息转发:就是重写方法3和4。

5.4.3 使用

_objc_msgForward是C语言函数,有三个参数 :

参数 类型
1. 所属对象 id类型
2. 方法名 SEL类型
3. 可变参数 可变参数类型

我们可以通过如下方式定义一个 IMP类型 :

typedef void (*voidIMP)(id, SEL, ...)

一旦调用_objc_msgForward,将跳过查找 IMP 的过程,直接触发“消息转发”,

如果调用了_objc_msgForward,即使这个对象确实已经实现了这个方法,你也会告诉objc_msgSend: “我没有在这个对象里找到这个方法的实现”

_objc_msgForward最常见的场景是:你想获取某方法所对应的NSInvocation对象:

JSPatch (Github 链接)就是直接调用_objc_msgForward来实现其核心功能的。

JSPatch 以小巧的体积做到了让JS调用/替换任意OC方法,让iOS APP具备热更新的能力。

番外:Clang

1、Clang ->.m文件编译成可执行文件的方法:

#import <Foundation/Foundation.h> 源文件:main.m

#import <Foundation/Foundation.h>
#import "JTest.h"
int main(int argc, char * argv[]) {
  
    @autoreleasepool {
        JTest *test = [[JTest alloc] init];
        [test performSelector:(@selector(sendSth))];
        NSLog(@"打印:3code.info");
        return 0 ;
    }
    
    
}

操作方法:

$ cd /test
$ clang -fobjc-arc -framework Foundation main.m -o nihau
-fobjc-arc表示编译需要支持ARC特性。
-framework Foundation表示引用Foundation框架。
-o nihao表示输出的可执行文件的文件名是nihau。

2、Clang ->编译.M文件为c++

$ clang -rewrite-objc main.m  即可生成.cpp文件

番外 self&super

解惑:这个题目主要是考察关于objc中对 self 和 super 的理解。

self 是类的隐藏参数,指向当前调用方法的这个类的实例。而 super 是一个 Magic Keyword, 它本质是一个编译器标示符,和 self 是指向的同一个消息接受者。上面的例子不管调用[self class]还是[super class],接受消息的对象都是当前 Son *xxx 这个对象。而不同的是,super是告诉编译器,调用 class 这个方法时,要去父类的方法,而不是本类里的。

当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中开始找。然后调用父类的这个方法。

** 按照如上规则,修改如下:**

   typedef NS_ENUM(NSInteger, JUserGender) {
       JUserGenderUnknown,
       JUserGenderMale,
       JUserGenderGay,
       JUserGenderFemale,
       JUserGenderLesbian,
       JUserGenderNeuter
   };

  @interface JUser : NSObject<NSCopying>

  @property (nonatomic, readonly, copy)      NSString    *name;
  @property (nonatomic, readonly, assign)    NSUInteger  age;
  @property (nonatomic, readwrite, assign)   JUserGender sex;

  - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(JUserGender)sex;
  + (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(JUserGender)sex;
  @end

支持一下

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码支持一下

打开支付宝扫一扫,即可进行扫码打赏哦