【靠谱程序员#1】《招聘一个靠谱的程序员》个人解答

599 阅读14分钟

系列目录

目录

1.风格纠错

2. 什么情况使用 weak 关键字,相比 assign 有什么不同?

1)、什么情况下使用weak
2)、与assign的不同
3)、参考资料
  a)、区别
  b)、相似
  c)、总结

3. 怎么用 copy 关键字?

4. 这个写法会出什么问题: @property (copy) NSMutableArray *array;

5. 如何让自己的类用 copy 修饰符?如何重写带 copy 关键字的 setter?

1)、让自己的类可以使用copy修饰符的步骤
2)、如何重写带copy关键字的setter:
3)、拓展
  a)、Copy & MutableCopy
  b)、深拷贝 & 浅拷贝

6. @property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的?

1)、@property 的本质是什么?
2)、拓展
3)、ivar、getter、setter 是如何生成并添加到这个类中的?

7. @protocol 和 category 中如何使用 @property

1)、在 protocol 中使用 property
2)、在category 中使用 @property

8. runtime 如何实现 weak 属性

1)、weak的特点
2)、runtime 如何实现 weak 变量的自动置nil?
3)、具体机制
4)、拓展
5)、小结

9. @property中有哪些属性关键字和修饰符?

10.weak属性需要在dealloc中置nil么?


1.风格纠错

纠错代码

答:

// 1)、使用NS_ENUM而不是C语言类型的枚举enum
typedef NS_ENUM(NSInteget, XBYGender) { //使用gender比sex正式
    XBYGenderMan,
    XBYGenderWoman
}

// 2)、一般定义一个初始化方法来对属性进行赋值,而把具体属性隐藏起来(放.m文件中),
// 或者设置为只读,不可外部修改(这里倾向于设置为只读)
// 3)、NSString一般使用copy修饰
@property(nonatomic, readonly, copy) NSString *name;
// 4)、和enum一样,尽量避免c语言的内容,将int改成NSInteger或者NSUInteger
@property(nonatomic, readonly, assign) NSUInteger age;
@property(nonatomic, readonly, assign) XBYGender gender;

// 5)、初始化一般是一个实例化方法一个工厂方法
// 6)、初始化应该把属性都有对应的赋值
// 7)、返回值一般用instancetype而不是id
+ (instancetype)initWithUserName:(NSString *)name age:(NSUInteger)age gender:(XBYGender)genger;
- (instancetype)initWithUserName:(NSString *)name age:(NSUInteger)age gender:(XBYGender)genger;

// 8)、login本来就有动作的意思,不需要do
- (void)login;

2. 什么情况使用 weak 关键字,相比 assign 有什么不同?

答:

1)、什么情况下使用weak

  • a、在ARC中修饰代理
@property (nonatomic, weak) id <BYDelegate> delegate;
  • b、使用@IBOutlet连接控件
@property (nonatomic, weak) IBOutlet UIView *view;
  • c、当block会造成循环引用
#import "People.h"

typedef void(^ResultBlock)(void);

@interface People ()

@property (nonatomic, copy) ResultBlock resultBlock;
@property (nonatomic, copy) NSString *name;

@end

@implementation People

- (void)printResult {
    self.resultBlock = ^{
        // 编译器警告:Capturing 'self' strongly in this block is likely to lead to
        // a retain cycle
        NSLog(@"%@", self.name);
    };
}

- (void)printName {
    __weak typeof(self) weakSelf = self;
    self.resultBlock = ^{
        NSLog(@"%@", weakSelf.name);
    };
}

@end

2)、与assign的不同

assign可以用于非对象类型,而weak必须用于对象类型

3)、参考资料

a)、区别

  • 1.修饰变量类型的区别
    weak 只可以修饰对象。如果修饰基本数据类型,编译器会报错-“Property with ‘weak’ attribute must be of object type”。 assign 可修饰对象,和基本数据类型。当需要修饰对象类型时,MRC时代使用unsafe_unretained。当然,unsafe_unretained也可能产生野指针,所以它名字是"unsafe_”。

  • 2.是否产生野指针的区别
    weak 不会产生野指针问题。因为weak修饰的对象释放后(引用计数器值为0),指针会自动被置nil,之后再向该对象发消息也不会崩溃。 weak是安全的。 assign 如果修饰对象,会产生野指针问题;如果修饰基本数据类型则是安全的。修饰的对象释放后,指针不会自动被置空,此时向对象发消息会崩溃。

b)、相似

  • 都可以修饰对象类型,但是assign修饰对象会存在问题。

c)、总结

  • assign 适用于基本数据类型如int,float,struct等值类型,不适用于引用类型。因为值类型会被放入栈中,遵循先进后出原则,由系统负责管理栈内存。而引用类型会被放入堆中,需要我们自己手动管理内存或通过ARC管理。
  • weak 适用于delegate和block等引用类型,不会导致野指针问题,也不会循环引用,非常安全。

3. 怎么用 copy 关键字?

答:

  • 1)、一般对于那些遵循NSCopying协议的类才能使用copy,例如针对那些有不可变类型和可变类型的类,NSString, NSArray 与之对应的是NSMutableString, NSMutableArray等。因为他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。
  • 2)、block一般也使用copy,这是MRC遗留下来的使用习惯

tips:

在 MRC中,方法内部的block 是在栈区的,使用 copy 可以把它放到堆区。在ARC 中写不写都
行:对于 block 使用copy还是strong效果是一样的,但写上copy也无伤大雅,还能时刻提醒
我们:编译器自动对 block 进行了 copy 操作。如果不写 copy,该类的调用者有可能会忘记
或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷
贝属性值。这种操作多余而低效。你也许会对这种做法有些怪异,不需要写却依然写。

示例:

@property (nonatomic, copy) NSString *userId;

- (instancetype)initWithUserId:(NSString *)userId {
   self = [super init];
   if (!self) {
       return nil;
   }
   _userId = [userId copy];
   return self;
}

4. 这个写法会出什么问题: @property (copy) NSMutableArray *array;

答:

  • 1)、copy修饰的属性进行赋值的时候会生成一份不可变的副本,此时修改副本array(增删之类的操作)会提示找不到方法而报异常;
  • 2)、不写原子性修饰词模式使用atomic,而atomic性能比nonatomic差很多。

5. 如何让自己的类用 copy 修饰符?如何重写带 copy 关键字的 setter?

答:

1)、让自己的类可以使用copy修饰符的步骤:

  • a)、让自己的类遵循NSCopying协议
  • b)、实现NSCopying协议中必须实现的代理方法:
- (id)copyWithZone:(NSZone *)zone;

示例代码:

- (id)copyWithZone:(NSZone *)zone {
    //创建实例的方式比较特殊
    Person *p = [[[self class] allocWithZone:zone] init];   
    
    p.name = self.name;
    p.age = self.age;
    
    return p;
}

2)、如何重写带copy关键字的setter:

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

3)、拓展

a)、Copy & MutableCopy

使用copy或mutableCopy方法可以创建一个对象的副本

  • copy 需要实现NSCoppying协议 这些创建的是不可变副本(如NSString、NSArray、NSDictionary)
  • mutableCopy 需要先实现NSMutableCopying协议 创建的是可变副本(如NSMutableString、NSMutableArray、NSMutableDictionary)
  • copy的目的 建立副本,同时修改原始对象和复本不会互相干扰

b)、深拷贝 & 浅拷贝

  • 深拷贝 内容拷贝,源对象和副本指向的是不同的两个对象 源对象引用计数器不变,副本计数器设置为1

  • 浅拷贝 指针拷贝,源对象和副本指向的是同一个对象 对象的引用计数器+1,其实相当于做了一次retain操作

结论

  • 只有不可变对象创建不可变副本(copy)才是浅复制,其他都是深复制

6. @property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的?

答:

1)、@property 的本质是什么?

@property = ivar + getter + setter

翻译出来就是: 属性(property)等于实例变量(ivar)加上存取方法(getter和setter,实例变量用于存储数据,而存取方法用来读取写入该实例变量的数据
有swift开发经验的人对这个感触应该表较深(存储属性和计算属性) 编译器会自动为@property添加实例变量和存取方法,当你重写一个存取方法的时候不用手动写实例变量:@synthesize name = _name;,但是当你重写setter和getter方法时,你需要添加这行代码才能通过编译。

2)、拓展

property在runtime中是objc_property_t定义如下:

typedef struct objc_property *objc_property_t;

而objc_property是一个结构体,包括name和attributes,定义如下:

struct property_t {
    const char *name;
    const char *attributes;
};

而attributes本质是objc_property_attribute_t,定义了property的一些属性,定义如下:

/// Defines a property attribute
typedef struct {
    const char *name;           /**< The name of the attribute */
    const char *value;          /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;

而attributes的具体内容是什么呢?其实,包括:类型,原子性,内存语义和对应的实例变量。

例如: 我们定义一个string的property@property (nonatomic, copy) NSString *string;,通过 property_getAttributes(property)获取到attributes并打印出来之后的结果为T@"NSString",C,N,V_string

其中T就代表类型,可参阅Type Encodings,C就代表Copy,N代表nonatomic,V就代表对于的实例变量。

Code Meaning
c A char
i An int
s A short
l A long l is treated as a 32-bit quantity on 64-bit programs.
q A long long
C An unsigned char
I An unsigned int
S An unsigned short
L An unsigned long
Q An unsigned long long
f A float
d A double
B A C++ bool or a C99 _Bool
v A void
* A character string (char *)
@ An object (whether statically typed or typed id)
# A class object (Class)
: A method selector (SEL)
[array type] An array
{name=type...} A structure
(name=type...) A union
bnum A bit field of num bits
^type A pointer to type
? An unknown type (among other things, this code is used for function pointers)

3)、ivar、getter、setter 是如何生成并添加到这个类中的?

“自动合成”( autosynthesis) 完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。需要强调的是,这个过程由编译 器在编译期执行,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码。除了生成方法代码 getter、setter 之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。在前例中,会生成两个实例变量,其名称分别为 _firstName 与 _lastName。也可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字.
@implementation Person

@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;

@end

反编译相关的代码,他大致生成了五个东西

OBJC_IVAR_$类名$属性名称 :该属性的“偏移量” (offset),这个偏移量是“硬编码” (hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。
settergetter 方法对应的实现函数
ivar_list :成员变量列表
method_list :方法列表
prop_list :属性列表

也就是说我们每次在增加一个属性,系统都会在 ivar_list 中添加一个成员变量的描述,在 method_list 中增加 setter 与 getter 方法的描述,在属性列表中增加一个属性的描述,然后计算该属性在对象中的偏移量,然后给出 setter 与 getter 方法对应的实现,在 setter 方法中从偏移量的位置开始赋值,在 getter 方法中从偏移量开始取值,为了能够读取正确字节数,系统对象偏移量的指针类型进行了类型强转.

7. 在@protocol 和 category 中如何使用 @property?

答:

1)、在 protocol 中使用 property

只会生成 setter 和 getter 方法声明,我们使用属性的目的,是希望遵守我协议的对象能实现该属性

2)、在 category 中使用 @property

也是只会生成 setter 和 getter 方法的声明,如果我们真的需要给 category 增加属性的实现,需要借助于运行时的两个函数:
objc_setAssociatedObject
objc_getAssociatedObject

Person.h

// 本类头文件及实现文件
#import <Foundation/Foundation.h>

@interface Person : NSObject

// 声明一个属性
@property (nonatomic, copy) NSString *name;

@end

Person.m

#import "Person.h"

@implementation Person

@end

Person+Extension.h

// 分类头文件及实现文件

#import "Person.h"

@interface Person (Extension)

// 分类中声明属性,只会生成setter和getter方法的声明,不会生成带“_”的成员变量
// 和setter和getter方法的实现
@property (nonatomic, assign) NSInteger age;

@end

Person+Extension.m

#import "Person+Extension.h"

// 使用运行时,需要导入头文件
#import <objc/runtime.h>

@implementation Person (Extension)

- (void)setAge:(NSInteger)age{
    // 使用运行时关联对象,person对象(self)强引用age对象,并且设置标记为"age"(可
    // 以根据该标记来获取引用的对象age,标记可以为任意字符,只要setter和getter中
    // 的标记一致就可以)
    // 参数1:源对象
    // 参数2:关联时用来标记属性的key(因为可能要添加很多属性),很多人习惯使用char *类
    // 型的key,如static char *key = "age",那这里需要传入&key,但是使用iOS的
    // NSString类型的字符常量也是可以的,目测估计这里需要内存地址一致,有兴趣可以
    // 试试给objc_setAssociatedObject的key对象 new一个NSString然后再
    // objc_getAssociatedObject的key new一个新的字面量相同的新的key对象
    // 参数3:关联的对象
    // 参数4:关联策略
    objc_setAssociatedObject(self, @"age", @(age), OBJC_ASSOCIATION_RETAIN);
}

- (NSInteger)age{
    // 根据“age”标识取person对象(self)强引用的age对象
    // 参数1:源对象
    // 参数2:关联时用来标记属性的key(因为可能要添加很多属性)
    return [objc_getAssociatedObject(self, @"age") integerValue];
}

@end

objc_AssociationPolicy的定义:

enum {
    OBJC_ASSOCIATION_ASSIGN = 0,            //assign
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,  //retain
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,    //copy
    OBJC_ASSOCIATION_RETAIN = 01401,
    OBJC_ASSOCIATION_COPY = 01403
};

8. runtime 如何实现 weak 属性

答:

1)、weak的特点

要实现 weak 属性,首先要搞清楚 weak 属性的特点:
weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种
属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同 assign 类似,然而在属
性所指的对象遭到摧毁时,属性值也会清空(nil out)。

2)、runtime 如何实现 weak 变量的自动置nil?

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

3)、具体机制

objc_storeWeak(&weakPo, Model)函数:

objc_storeWeak函数把赋值对象(Model)的内存地址作为键值key,将weak修饰的属性
变量(weakPo)的内存地址(& weakPo)作为value,注册到 weak 表中。

如果Model为0(nil),那么把变量(weakPo)的内存地址(& weakPo)从weak表中删除。

可以把objc_storeWeak(&weakPo, Model)理解为:objc_storeWeak(value, key),并
且当key变nil,将value置nil。

在Model非nil时,weakPo和Model指向同一个内存地址,在Model变nil时,weakPo变nil。
此时向weakPo发送消息不会崩溃:在Objective-C中向nil发送消息是安全的。

4)、拓展

#import <Foundation/Foundation.h>

@interface WeakProperty : NSObject

@property (nonatomic,weak) NSObject *obj;


@end

@implementation WeakProperty

- (void)dealloc {
    NSLog(@"%s",__func__);
}

@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        WeakProperty *property = [[WeakProperty alloc] init];
        NSObject *obj = [[NSObject alloc] init];
        property.obj = obj;     
        NSLog(@"%@",property.obj);   

        // 会触发函数 ``id objc_initWeak(id *location, id newObj)``       
        // NSObject *obj = [[NSObject alloc] init];
        // __weak NSObject *obj2 = obj;
        // 会触发函数 ``void objc_copyWeak(id *dst, id *src)``
        // __weak NSObject *obj3 = obj2;
    }
    return 0;
}

结果

对象的 weak 属性调用 setter 时,调用

id objc_storeWeak(id *location, id newObj)
static id storeWeak(id *location, objc_object *newObj) 

使用 NSLog 输出 property.obj 属性时,调用

id objc_loadWeakRetained(id *location)

当 dealloc 释放对象时,调用

void objc_destroyWeak(id *location)

相关函数 查看 NSObject.mm 源码发现

id objc_storeWeak(id *location, id newObj)
id objc_storeWeakOrNil(id *location, id newObj)
id objc_initWeak(id *location, id newObj)
id objc_initWeakOrNil(id *location, id newObj)
void objc_destroyWeak(id *location)

都调用了 static id storeWeak(id *location, objc_object *newObj), objc_xxxWeakOrNil 多了一点额外的处理。而void objc_destroyWeak(id *location)在调用 static id storeWeak(id *location, objc_object *newObj)时 newObj 参数传递的是 nil 这一点与上面提到的参考答案中关于 dealloc 释放对象时,将哈希表中指定的键对应的值设置为 nil 是符合的。

5)、小结

  • storeWeak 函数用于为 weak 属性赋值 (包括销毁)
  • objc_loadWeakRetained 函数用于获取 weak 属性

对于函数 storeWeak 的调用

  • 赋值,即 id objc_storeWeak(id *location, id newObj)
  • 销毁,即 void objc_destroyWeak(id *location)

而对于 weak 属性的获取的调用方式

  • 函数 id objc_loadWeakRetained(id *location)

9. @property中有哪些属性关键字和修饰符?

答:

1)、原子性---(atomic/nonatomic ,默认或不写是atomic)

在默认情况下,由编译器合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具
备 nonatomic 特质,则不使用自旋锁。

2)、读写权限---(readwrite/readonly)

3)、内存管理---(strong/weak/assign/unsafe_unretain/copy)

4)、读写方法名---(get=< name >/set=< name >)

@property (nonatomic, getter=isOn) BOOL on;

5)、不常用---(nonnull/null_resettable/nullable)


注意

很多人会认为如果属性具备 nonatomic 特质,则不使用 “同步锁”。其实在属性设置方法中使用的是自旋锁。
自旋锁相关代码如下:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
   if (offset == 0) {
       object_setClass(self, newValue);
       return;
   }

   id oldValue;
   id *slot = (id*) ((char*)self + offset);

   if (copy) {
       newValue = [newValue copyWithZone:nil];
   } else if (mutableCopy) {
       newValue = [newValue mutableCopyWithZone:nil];
   } else {
       if (*slot == newValue) return;
       newValue = objc_retain(newValue);
   }

   if (!atomic) {
       oldValue = *slot;
       *slot = newValue;
   } else {
       spinlock_t& slotlock = PropertyLocks[slot];
       slotlock.lock();
       oldValue = *slot;
       *slot = newValue;        
       slotlock.unlock();
   }

   objc_release(oldValue);
}

void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
   bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
   bool mutableCopy = (shouldCopy == MUTABLE_COPY);
   reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}

10.weak属性需要在dealloc中置nil么?

在ARC中,无论是strong修饰的对象还是weak修饰的对象,都不再需要在dealloc中将对象置为nil,编译器会自动帮我们处理,即使编译器不帮助我们处理,根据 《8. runtime 如何实现 weak 属性》,我们可以知道,在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。

下一篇:【靠谱程序员#2】《招聘一个靠谱的程序员》个人解答