1、分类和扩展有什么区别?可以分别用来做什么?分类有哪些局限性?分类的结构体里面有哪些成员?
答:
①类别中原则上只能增加方法(能添加属性的的原因只是通过runtime解决无setter/getter的问题而已);
②类扩展不仅可以增加方法,还可以增加实例变量(或者属性),只是该实例变量默认是@private类型的(
用范围只能在自身类,而不是子类或其他地方);
③类扩展中声明的方法没被实现,编译器会报警,但是类别中的方法没被实现编译器是不会有任何警告的。这是因为类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中。
④类扩展不能像类别那样拥有独立的实现部分(@implementation部分),也就是说,类扩展所声明的方法必须依托对应类的实现部分来实现。
⑤定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明私有方法的非常好的方式。
最重要的还是类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中。
分类方法未实现,编译器也不会报警告。
分类方法与原类中相同会优先调用分类。
分类结构体:
typedef struct objc_category *Category;
struct objc_category {
char *category_name OBJC2_UNAVAILABLE; // 分类名
char *class_name OBJC2_UNAVAILABLE; // 分类所属的类名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 实例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 类方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}
2、讲一下atomic的实现机制;为什么不能保证绝对的线程安全(最好可以结合场景来说)?
(1)atomic是线程安全的,但它运行效率慢,noatomic不是线程安全的它他效率高
(2)atomic的seter getter内部实现是用了互斥锁来保证seter getter方法在多线程中的安全
(3)nonatomic对象setter和getter方法的实现并么有加互斥锁,所以nonatomic修饰的对象是非线程安全的,同时nonatomic对象setter和getter方法也是非线程安全的,但也正因为没有互斥锁所以性能要比atomic好
3、互斥锁与自旋锁的区别?
互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销。
自旋锁:线程一直是running(加锁——>解锁),死循环检测锁的标志位,机制不复杂
自旋锁缺点:
1、自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低
2、在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等
3、被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么?
被weak修饰的对象在被释放时候会置为nil,不同于assign;
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组。
1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
struct SideTable {
// 保证原子操作的自旋锁
spinlock_t slock;
// 引用计数的 hash 表
RefcountMap refcnts;
// weak 引用全局 hash 表
weak_table_t weak_table;
}
struct weak_table_t {
// 保存了所有指向指定对象的 weak 指针
weak_entry_t *weak_entries;
// 存储空间
size_t num_entries;
// 参与判断引用计数辅助量
uintptr_t mask;
// hash key 最大偏移值
uintptr_t max_hash_displacement;
};
4、Copy和Strong修饰属性
数据源为可变字符串而言,使用copy申明属性,会开辟一块新的内存空间存放值,源数据不论怎么变化,都不会影响copy属性中的值,属于深拷贝;使用strong申明属性,不会开辟新的内存空间,只会引用到源数据内存地址,因此源数据改变,则strong属性也会改变,属于浅拷贝
当原字符串是NSString时,由于是不可变字符串,所以,不管使用strong还是copy修饰,都是指向原来的对象,copy操作只是做了一次浅拷贝。
5、block 修饰copy strong
_NSConcreteGlobalBlock 全局的静态 block,不会访问外部局部变量(显然包括无外部变量或者全局变量)。
_NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。
_NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁
- block内部没有调用外部局部变量时存放在全局区(ARC和MRC下均是)
- block使用了外部局部变量,这种情况也正是我们平时所常用的方式。MRC:Block的内存地址显示在栈区,栈区的特点就是创建的对象随时可能被销毁,一旦被销毁后续再次调用空对象就可能会造成程序崩溃,在对block进行copy后,block存放在堆区.所以在使用Block属性时使用copy修饰。但是ARC中的Block都会在堆上的,系统会默认对Block进行copy操作
- 用copy,strong修饰block在ARC和MRC都是可以的,都是在堆区
补充:一个block要使用self,会处理成在外部声明一个weak变量指向self,然而为何有时会出现在block里又声明一个strong变量指向weakSelf?
原因:block会把写在block里的变量copy一份,如果直接在block里使用self,(self对变量默认是强引用)self对block持有,block对self持有,导致循环引用,所以这里需要声明一个弱引用weakSelf,让block引用weakSelf,打破循环引用。
而这样会导致另外一个问题,因为weakSelf是对self的弱引用,如果这个时候控制器pop或者其他的方式引用计数为0,就会释放,如果这个block是异步调用而且调用的时候self已经释放了,这个时候weakSelf已就变成了nil。
当控制器(也可以是其他的控件)pop回来之后(或者一些其他的原因导致释放),网络请求完成,如果这个时候需要控制器做出反映,需要strongSelf再对weakSelf强引用一下。
但是,你可能会疑问,strongSelf对weakSelf强引用,weakSelf对self弱引用,最终不也是对self进行了强引用,会导致循环引用吗。不会的,因为strongSelf是在block里面声明的一个指针,当block执行完毕后,strongSelf会释放,这个时候将不再强引用weakSelf,所以self会正确的释放
6、关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将所有的关联对象的指针置空么?
可以不改变源码的情况下增加实例变量。
可与分类配合使用,为分类增加属性。(类别是不能添加成员变量的(property本质也是成员变量 = var + setter、getter),原因是因为一个类的内存大小是固定的,一个雷在load方法执行前就已经加载在内存之中,大小已固定)
AssociationsManager
AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。这相当于把所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个map的value又是另外一个AssociationsHashMap,里面保存了关联对象的kv对。
class AssociationsManager {
static OSSpinLock _lock;
static AssociationsHashMap *_map; // associative references: object pointer -> PtrPtrHashMap.
public:
AssociationsManager() { OSSpinLockLock(&_lock); }
~AssociationsManager() { OSSpinLockUnlock(&_lock); }
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
销毁
在obj dealloc时候会调用object_dispose,检查有无关联对象,有的话_object_remove_assocations删除
id
object_dispose(id obj)
{
if (!obj) return nil;
// 销毁对象
objc_destructInstance(obj);
// 释放内存
free(obj);
return nil;
}
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
// C++ 析构
if (cxx) object_cxxDestruct(obj);
// 移除 Associated Object
if (assoc) _object_remove_assocations(obj);
// ARC 下调用实例变量的 release 方法,移除 weak 引用
obj->clearDeallocating();
}
return obj;
}
7、KVO的底层实现?如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO)?
当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法。setter 方法随后负责通知观察对象属性的改变状况。
Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A 的新类,该类继承自对象A的本类,且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。
修改
使用方法,可实现取消系统kvo,自己触发,也就可控。
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"name"]) {
return NO;
}else{
return [super automaticallyNotifiesObserversForKey:key];
}
}
-(void)setName:(NSString *)name{
if (_name!=name) {
[self willChangeValueForKey:@"name"];
_name=name;
[self didChangeValueForKey:@"name"];
}
}
8、Autoreleasepool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么?
在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop
2530231-3532aca36384ac8c.jpg
- AutoreleasePool并没有单独的结构,而是由若干个
AutoreleasePoolPage以双向链表
的形式组合而成(分别对应结构中的parent指针和child指针)。* AutoreleasePool是按线程一一对应
的(结构中的thread指针指向当前线程)。* AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
。* 上面的id next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
。 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入。
9、iOS 中内省的几个方法?class方法和objc_getClass方法有什么区别?
判断对象类型:
-(BOOL) isKindOfClass: 判断是否是这个类或者这个类的子类的实例
-(BOOL) isMemberOfClass: 判断是否是这个类的实例
判断对象or类是否有这个方法
-(BOOL) respondsToSelector: 判读实例是否有这样方法
+(BOOL) instancesRespondToSelector: 判断类是否有这个方法
object_getClass:获得的是isa的指向
self.class:当self是实例对象的时候,返回的是类对象,否则则返回自身。
类方法class,返回的是self,所以当查找meta class时,需要对类对象调用object_getClass方法
10、RunLoop的作用是什么?它的内部工作机制了解么?
runloop原理
11、哪些场景可以触发离屏渲染?
- shouldRasterize(光栅化)
- masks(遮罩)
- shadows(阴影)
- edge antialiasing(抗锯齿)
- group opacity(不透明)
- 复杂形状设置圆角等
- 渐变
12、AppDelegate如何瘦身?
苹果的官方文档都建议应该由AppDelegate来处理这些工作:
1.app的启动代码;
2.响应app的状态,比如app切换到后台和前台等状态;
3.响应外部传递给app的通知,比如说push,low-memory warnings;
4.决定了app的状态是否应该保存或者恢复;
5.响应不是发送给特定view或者vc,而是发送给app本身的事件;
6.用来保存一些不属于特定vc的数据。
第三方一些瘦身的库
13、反射是什么?可以举出几个应用场景么?
系统Foundation框架为我们提供了一些方法反射的API,我们可以通过这些API执行将字符串转为SEL等操作。由于OC语言的动态性,这些操作都是发生在运行时的。
// SEL和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector);
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);
// Class和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromClass(Class aClass);
FOUNDATION_EXPORT Class __nullable NSClassFromString(NSString *aClassName);
// Protocol和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromProtocol(Protocol *proto) NS_AVAILABLE(10_5, 2_0);
FOUNDATION_EXPORT Protocol * __nullable NSProtocolFromString(NSString *namestr) NS_AVAILABLE(10_5, 2_0);
// 假设从服务器获取JSON串,通过这个JSON串获取需要创建的类为ViewController,并且调用这个类的getDataList方法。
Class class = NSClassFromString(@"ViewController");
ViewController *vc = [[class alloc] init];
SEL selector = NSSelectorFromString(@"getDataList");
[vc performSelector:selector];
14、App 启动优化策略?最好结合启动流程来说
App启动过程
解析Info.plist
加载相关信息,例如如闪屏
沙箱建立、权限检查
Mach-O加载
如果是胖二进制文件,寻找合适当前CPU类别的部分
加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
定位内部、外部指针引用,例如字符串、函数等
执行声明为attribute((constructor))的C函数
加载类扩展(Category)中的方法
C++静态对象加载、调用ObjC的 +load 函数
程序执行
调用main()
调用UIApplicationMain()
调用applicationWillFinishLaunching
main之前的优化
动态库加载越多,启动越慢。
ObjC类越多,启动越慢
C的constructor函数越多,启动越慢
C++静态对象越多,启动越慢
ObjC的+load越多,启动越慢
main之后的优化
rootViewController及其childViewController的加载、view及其subviews的加载
具体做法可以打点记录各种vc view的初始化时间。
主要还是针对不同业务的优化,在我的项目中,有个后台串行的队列,去初始化各种不需要立即加载的资源,注册各种三方。
15、App 无痕埋点的思路了解么?你认为理想的无痕埋点系统应该具备哪些特点?
无痕埋点就是记录所有的事件,需要的时候去查询。
可分为两种
用户点击事件
button 手势的点击,这个可以hook相关的方法,addtarget 等,去埋点,通过view获取vc名,view的层级信息(在vc的第几个subview层级)
事件 hook的系统类 hook的系统方法
按钮的点击 UIApplication sendAction:to: from:forEvent:
手势操作 UIGestureRecognizer initWithTarget:action: addTarget:action:
列表点击 UITableView和UICollectionView setDelegate:、tableView:didSelectRowAtIndexPath:、collectionView:didSelectItemAtIndexPath:等
系统弹窗 UIAlertView setDelegate:、alertView:clickedButtonAtIndex:
(2)页面事件拦截
事件 hook的系统类 hook的系统方法
页面事件 UIVIewController viewDidLoad 、viewWillAppear: 、viewDidAppear: 、viewWillDisappear:等生命周期方法:
系统导航栏返回按钮 UINavigationController navigationBar:didPopItem:
非点击事件
进入,离开vc的信息,这个hook vc的相应方法也可达到。
如何使用
需要查找某个按钮的点击,需要在相应版本上获取按钮所在vc的subview层级信息,去上报系统中查询。
log上报
无痕埋点log量是很大的,实时上传是不可取的。我的方案是,后台写入log,wifi环境下达到一定大小上传(2m)。
16、有哪些情况会导致app崩溃,分别可以用什么方法拦截并化解?
1、NSInvalidArgumentException 异常
向容器加入nil,引起的崩溃。hook容器添加方法,进行判断。
https://github.com/jasenhuang/NSObjectSafe
2、 SIGSEGV 异常
SIGSEGV是当SEGV发生的时候,让代码终止的标识。 当去访问没有被开辟的内存或者已经被释放的内存时,就会发生这样的异常。另外,在低内存的时候,也可能会产生这样的异常。
3、 NSRangeException 异常
造成这个异常,就是越界异常了,在iOS中我们经常碰到的越界异常有两种,一种是数组越界,一种字符串截取越界
4、SIGPIPE 异常
先解释一下什么是SIGPIPE异常,通俗一点的描述是这样的:对一个端已经关闭的socket调用两次write,第二次write将会产生SIGPIPE信号,该信号默认结束进程。
SIGABRT 异常 这是一个让程序终止的标识,会在断言、app内部、操作系统用终止方法抛出。通常发生在异步执行系统方法的时候。如CoreData、NSUserDefaults等,还有一些其他的系统多线程操作。 注意:这并不一定意味着是系统代码存在bug,代码仅仅是成了无效状态,或者异常状态。
17、有哪些情况会导致app卡顿,分别可以用什么方法来避免?
分cpu卡和gpu卡顿。
主线程耗时操作
线程爆炸
滑动页面渲染卡顿(离屏渲染)
图像渲染解码
查看xcode的cpu占用。
使用instrument 查看耗时代码。查看渲染耗时问题。
18、App 网络层有哪些优化策略?
Api请求过程
当我们向服务器发送一个请求的时候,做了以下事情:
1.DNS Lookup
2.TCP Handshake
3.TLS或SSL Handshake
4.TCP/HTTP Request/Response
1、优化DNS解析和缓存
2、网络质量检测(根据网络质量来改变策略)
3、提供网络服务优先级和依赖机制
4、提供网络服务重发机制
5、减少数据传输量
6、优化海外网络性能
19、了解编译的过程么?分为哪几个步骤?
LLVM编译器
源代码 --> 预处理器 --> 编译器 --> 汇编 --> 机器码 --> 链接 --> 可执行文件
clang是实际的编译命令
-x objective-c 指定了编译的语言
-arch x86_64制定了编译的架构,类似还有arm7等
-fobjc-arc 一些列-f开头的,指定了采用arc等信息。这个也就是为什么你可以对单独的一个.m文件采用非ARC编程。
-Wno-missing-field-initializers 一系列以-W开头的,指的是编译的警告选项,通过这些你可以定制化编译选项
-DDEBUG=1 一些列-D开头的,指的是预编译宏,通过这些宏可以实现条件编译
-iPhoneSimulator10.1.sdk 制定了编译采用的iOS SDK版本
-I 把编译信息写入指定的辅助文件
-F 链接所需要的Framework
-c ClassName.c 编译文件
-o ClassName.o 编译产物
过程如果下:
链接需要的Framework,例如Foundation.framework,AFNetworking.framework,ALiPay.fframework
编译xib文件
拷贝xib,图片等资源文件到结果目录
编译ImageAssets
处理info.plist
执行CocoaPod脚本
拷贝Swift标准库
创建.app文件和对其签名
最后生成二进制文件
21、静态链接了解么?静态库和动态库的区别?
库:库就是写好的、现有的、成熟的程序代码的集合。
静态库:链接时会被完整的复制到可执行文件中,被多次使用就有多份拷贝。
动态库:链接时不复制,程序运行时由系统动态加载到内存,系统只加载一次,多个程序共用,节省内存。
22、内存的几大区域,各自的职能分别是什么?
-
栈区
: 局部变量和方法实参*堆区
:OC中使用new方法创建的对象,被创建对象的所有成员变量保存在堆区中.*BSS段(也叫静态区)
:
教科书
:未被初始化的全局变量和静态变量.
Xcode8中
: 全局变量和静态变量,不管有没有被初始化,都存放在BSS段中.验证见本篇中的案例.*常量区(也叫数据段)
:
教科书: 存储已经初始化的全局变量,静态变量,常量.
xcode8: 存储常量*代码段
: 程序的代码.
网友评论