2022加油(fmyz)
一、基础
1.说出常用的属性修饰关键字
原子性和非原子性
-
nonatomic
非原子操作。决定编译器生成的setter和getter方法是否是原子操作。atomic表示多线程安全,一般使用nonatomic
,效率高。
读写属性
-
readwrite
是可读可写特性。需要生成getter方法和setter方法。 -
readonly
是只读特性。只会生成getter方法,不会生成setter方法,不希望属性在类外改变。
内存属性
-
assign
是赋值特性。assign一般用于修饰基本数据类型。 -
retain(MRC)/strong(ARC)
表示持有特性。引用计数加1。一般修饰对象类型。 -
copy
表示拷贝特性。。 -
weak
弱引用,引用计数不会加1。对象释放的时候指针自动重置为nil。多用来解决循环引用问题。如delegate和xib连出来的空间
2.atomic原子性是绝对线程安全的么?
- atomic的本质是保证get set方法的线程安全,并不是保证修饰的对象的线程安全。
- atomic与nonatomic的本质区别其实也就是在setter方法上的操作不同,atomic保证了getter和setter存取方法的线程安全,两者都不能保证整个对象是线程安全的。
- nonatomic的速度要比atomic的快。
3.说下Category原理,以及为什么只能添加方法不能添加属性?
-
category_t
结构体主要包含方法列表、属性列表、协议列表,不包含成员变量列表。 - 分类实现是将分类中的方法、属性、协议放在
category_t
结构体中,运行时结构体中的方法拷贝到对象的方法列表methodList
中。 - 可以添加属性,但不会生成set、get方法,因为
Category_t
结构体中不存在成员变量列表。成员变量列表是存在实例对象结构体中的,并在编译阶段就已经决定好了。分类是在运行时才去加载的,无法拷贝属性到对象的成员变量列表。 -
category_t
结构体
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; // 属性
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
4.load 和 initialize的区别
(1)调用时机:
-
load
方法在pre-main
阶段当装载类信息的时候就调用 -
initialize
是当类第一次接收到消息的时候。
(2)调用顺序:
-
load
方法的调用顺序为父类load > 子类 load > 分类load,且都会调用; -
initialize
调用顺序为父类initialize > 子类或者分类initialize,且父类initialize只会调用一次,如果有分类initialize会覆盖子类的initialize
(3)调用方式
-
load
方法是通过isa指针找到对应的方法与实现 -
initialize
方法是通过runtime的消息转发机制调用的
(4)实际使用
-
load
方法一般用于方法交换 -
initialize
方法一般用于初始化全局变量或静态变量。
5.分类和扩展之间的区别
(1)Category
- 不可以添加属性,需要通过
runtime
添加属性。 - 如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会忽略原有类的方法。所以同名方法调用的优先级为 分类 > 本类 > 父类。因此在开发中尽量不要覆盖原有类
- 如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法
(2)Extension
-
Extension
是Category
的一个特例。类扩展与分类相比只少了分类的名称,所以称之为“匿名分类”。一般写在.m文件中,用来扩展私有属性和方法。 - 定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明私有方法的非常好的方式。
(3)区别
-
Category
只可以添加方法,Extension可以添加属性和方法。 -
Extension
只能用于自身类,而不能用于子类或者其他地方。 -
Extension
中声明的方法必须要实现,否则编译器警告。Category
不会。这是因为Extension
是编译阶段被添加到类中,Category
是运行时被添加到类中。
6.多个分类中都有相同的方法名,执行那个分类?
- 和编译顺序有关,执行最后一个编译的分类
- 可以在
buildPhases->Compile Sources
里面调整编译顺序
7.给Category
设置关联对象实现原理
- 关联对象并不是放在了原来的对象里面,而是自己维护了一个全局的map用来存放每一个对象及其对应关联属性表格
- 关联对象并不是存储在被关联对象本身内存中,而是存储在全局的统一的一个AssociationsManager中,如果设置关联对象为nil,就相当于是移除关联对象
8.控制器的生命周期
参考链接
9.怎么样让自己的对象用copy修饰
- 实现
NSCopying
协议并实现-(id)copyWithZone(NSZone *)zone
协议方法 - 如有可变类型需要实现
NSMutableCopying
协议并实现协议方法
10.如何创建一个单例
- 1.首先GCD实现单例,只要我们在外面调用shareManager这个方法,返回的对象始终是一个,因为dispatch_once只执行一次
static DataManager *manager = nil;
@implementation DataManager
+ (instancetype)shareManager{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[self alloc] init];
});
return manager;
}
@end
- 2.如果我们不小心调用了
alloc
、allocWithZone
方法,就不是同一个对象了。调用alloc
方法时,会自动调用allocWithZone
方法,此时只需要重写AllocWithZone
即可
+ (id)allocWithZone:(struct _NSZone *)zone{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [super allocWithZone:zone];
});
return manager;
}
3.不小心调用了copy、mutableCopy方法(需要实现NSCopying
、NSMutableCopying
协议实现copyWithZone
、mutableCopyWithZone
代理方法,否则崩溃)
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
return manager;
}
- (nonnull id)mutableCopyWithZone:(nullable NSZone *)zone {
return manager;
}
参考链接
二、项目架构
1.RAC关键字用法
- RAC中关键字用法
三、网络类
1.讲一下https通信过程
参考链接
四、内存管理
1.内存中的五大区
- 栈:局部变量、函数的参数、对象的指针。运行时分配。栈是由编译器自动分配并释放。
- 堆:使用
alloc
、new
生成的对象。运行时分配。堆是由程序员分配和释放。 - 全局区、静态区:包含为初始化的数据和已经初始化的数据。编译时分配
- 常量区:常量字符串。编译时分配
- 代码:存放程序的二进制代码。编译时分配
2.内存管理方式
(1)TaggedPointer
- 对于小对象如
NSNumber
,指针指向对象,不再指向地址
(2)NONPOINTER_ISA(非指针型的ISA)
- 参考链接
(3) SideTables 散列表
- 自旋锁
- 引用计数表
- 弱引用表
3.ARC&MRC
ios开发中是通过引用计数
来进行对象内存管理的。主要分为两种方式
-
MRC
: 通过手动引用计数来进行对象的内存管理 -
ARC
: 通过自动引用计数来管理内存。编译器LLVM和Runtime(weak对象的释放)来共同协作在对应的位置插入相应的retain和release操作,实现了ARC的全部功能。ARC中新增weak, strong属性关键字。 -
retain
release
retainCount
dealloc
内部实现 参考链接
以下是MRC中常用到的几个关键字
- alloc : 分配对象的内存空间。
- retain : 使一个对象的引用计数加1
- release : 使对象的引用计数减1
- retainCount : 获取当前对象的引用计数值
- autorelease : 当前对象会在autoreleasePool结束的时候,调用这个对象的release操作,进行引用计数减1
- dealloc : 在MRC中若调用dealloc,需要显示的调用[super dealloc],来释放父类的相关成员变量
4.weak的实现原理
- 初始化时:调用
objc_initweak
函数,初始化一个心得weak指针指向对象地址 - 添加引用时:
objc_initweak
调用objc_storeWeak
函数。objc_storeWeak
的作用是更新指针指向,创建对应的弱引用表。objc_storeWeak
调用weak_register_no_lock
函数,讲weak指针添加到弱引用表中。添加的位置是通过哈希算法查找的。 - 释放时:调用
clearDeallocating
函数,clearDeallocating
函数通过调用weak_clear_no_lock
方法根据对象地址获取所有weak
指针数组,然后遍历该数组将其中数据置为nil,最后把这个entry
从weak表中移除,最后清理对象记录。
弱引用表结构
struct weak_table_t {
// 保存了所有指向指定对象的 weak 指针
weak_entry_t *weak_entries;
// 存储空间
size_t num_entries;
// 参与判断引用计数辅助量
uintptr_t mask;
// hash key 最大偏移值
uintptr_t max_hash_displacement;
};
- 参考1
- 参考2
5.什么情况下会导致内存泄漏
- NSTimer :self 持有 timer,timer 在初始化时持有 self,造成循环引用。 解决的方法就是,在dealloc方法以外,使用 invalidate 方法销掉 timer。
- block : block中使用self时,会导致self,block的互相持有,无法释放。__weak修饰
代理使用Strong修饰也会导致 - 通知及kvo没有移除监听也会导致内存泄漏
- OC中直接运用C语言 (静态分析analyze)
五、Block
1.block用什么关键字修饰?
MRC情况下
:
Block分为三种类型,全局、栈区、堆区。当没有访问外部变量的时候属于全局block,访问局部变量且没有被copy时属于栈区block,访问局部变量且copy后属于堆区block。栈区内存可能随时被回收和释放,释放后再对该block引用,会造成系统崩溃。使用 copy 修饰,会将栈区的 block 拷贝到堆区,retain修饰会在栈区 ,所以使用copy。ARC情况下
:
当没有访问外部变量的时候属于全局block,访问局部变量且没有强指针指向的时候属于栈区block,
访问局部变量且有强指针指向的时候属于堆区block。用strong、 copy修饰的都在堆区,所以用copy strong都可以。平常使用都用copy修饰,主要是strong是ARC时期引入的,开发者早已在MRC中习惯使用copy来修饰block。block建立在栈上,而不是堆上,这么做一个是为性能考虑,还有就是方便访问局部变量。
参考链接
六、进程线程
1. 进程和线程区别
- 进程:是一个有独立功能的程序,可以理解为手机上的一个app。每个进程是相互独立的,每个进程运行在受保护的内存空间。
- 线程:进程执行的最小单位,一个进程要执行任务最少开启一条线程(主线程)。线程是cpu分配资源和调度的最小单位。
- 区别:(1)线程是进程的执行单元,进程中所有任务都在线程中执行。(2)同一个进程内的线程共享进程资源
七、cocoapods
- 参考链接1
八、RunLoop
1.自动释放池与runloop关系?
- 第一个
Observer
监视的事件是Entry(即将进入Loop)
,其回调内会调用_objc_autoreleasePoolPush()
创建自动释放池。优先级最高,保证创建释放池发生在其他所有回调之前。第二个Observer
监视了两个事件:BeforeWaiting(准备进入休眠)
时调用_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
释放旧的池并创建新池;Exit(即将退出Loop)
时调用_objc_autoreleasePoolPop()
来释放自动释放池。优先级最低,保证其释放池子发生在其他所有回调之后。 - 顺序就是
Entry-->push ---> BeforeWaiting--->pop-->push -->Exit-->pop
,按照这样的顺便,保证了,没一次push都对应一个pop。
2.请指出下面这段代码问题
for (int i = 0; i < 10000; i++ ) {
NSString *str = @"ABC";
str = [str lowercaseString];
str = [str stringByAppendingString:@"xyz"];
NSLog(@"%@",str);
}
- 内存得不到及时地释放,内存暴涨.RunLoop是在每个事件循环结束后才会自动释放池去使对象的引用计数减一,对于引用计数为0的对象才会真正被销毁、回收内存。正确的写法如下
for ( int i = 0 ; i < 10000 ; ++ i ) {
@ autoreleasepool {
NSString *str = @"Abc" ;
str = [ str lowercaseString ] ;
str = [ str stringByAppendingString : @"xyz" ] ;
NSLog ( @"%@" , str ) ;
}
}
九、性能优化
1.降低app包大小
- 利用 AppCode 检测未使用的代码:菜单栏 -> Code -> Inspect Code
- 无损压缩项目中的图片资源,删除无用资源图片
- 编译器优化项目优化(编译器优化级别、去除符号信息)
- 去掉舍弃架构armv7
- 去掉oc与swift混编
2. 正确的复用cell
- 尽量避免使用透明色
- 动态计算并缓存行高,避免重新布局
- 加载网络数据,使用异步加载,缓存请求结果
- 滑动很快时,按需加载范围内的cell
- 刷新使用reloadSection
- cell中的控件尽量少,避免动态的添加视图
- 不要做多余的绘制工作
- 尽量少用xib
3.APP的启动优化
优化的时候会将优化以main()函数为界分为2个部分,即main之前的pre-main阶段 和 main()之后。
(1) pre-main阶段这个阶段是由dyld(动态连接器)来操作的,设置DYLD_PRINT_STATISTICS
进行检测这个阶段耗时。
dylib loading time: 动态库加载耗时(169.23ms)。关于动态库的加载,这个是不可避免的,我们能做的就是减少动态库的引用,官方的建议的是动态库的使用应该在6个以内,所以这里就引入了一个动态库合并的概念,通过合并动态库,从而减少在pre-main时的加载时间。
rebase/binding: 偏移修正/符号绑定。这个过程由操作系统完成。(ASLR安全机制,在二进制文件头部添加随机值)/
ObjC setup: OC类注册。这也就意味着项目中OC类越多,这里消耗的时间也就会增加。
initializer: 这个阶段指的是
+ (void)load
,C++
构造函数等初始化操作。 这里可以看到用时5.1 seconds,是所有项做高的。这里是因为我在项目里面随便的一个类里实现了+(void)load函数,并模拟了一个耗时操作。所以这里的优化比较明确:1. 能不使用+load就尽量不要使用,可以将load内部逻辑推迟到initialize时;2. 使用到了load,就尽量不要在内部执行耗时操作;3. 如果混编了C++代码,要尽量减少构造函数中的耗时操作slowest intializers: 启动时用时最慢的文件,这个可以看到耗时最多的是TestApp项目本身,这里主要是由于那个模拟的耗时操作导致。
(2) 在main()函数之后的优化就因项目不同而异了,大致有这么几个核心:
- 业务逻辑:这里主要指APP从启动到首页呈现的阶段。尽量减少与该阶段无关且没有必要的初始化代码操作,把这部分代码以懒加载的方式处理。
- 删除无用代码:这里是随着业务的发展,APP不断的迭代更新,会产生很多的的下架业务,从而堆积了很多的无用代码,这些代码会增加ObjC setup的耗时,所以要清理掉。
- 多线程操作:在启动时,将一些必要的非UI业务且需要初始化操作的任务放在子线程中,这样可以在APP启动的时候,发挥CPU的最大性能。
- 启动页面:首要呈现的画面,尽量减少使用 .xib 或storyBoard来实现,因为它们需要解析成代码,会造成耗时。
文章参考