本篇主要针对面试题进行解析,会进行基础知识的总结和拓展,仅供参考,如有错误,欢迎指出,一起学习!
一、关于Foundation框架中的问题
(一)NSCache & NSDictionary
1.NSDictionary(字典)
字典是由键-值(key–value)组成的数据集合,其中值为对象,可以通过键从字典中获取需要的值。字典中的键必须唯一,通常情况下,键为字符串对象,主要用于注明存储对象的说明,但键也可以是其他类型的对象,和键相关联的值可以是任何类型的对象,但是不能是nil。
NSDictionary *dict = @{
@"website":@"www.99ios.com",
@"name":@"九九学院",
@"business":@"ios学习",
@"founderYear":@2016,
};
NSLog(@"第一种类型创建的字典为:%@",dict);
方式二:注意其顺序,是value,key,并且以nil结尾,打印顺序是从后往前
NSDictionary *dict1 = [NSDictionary dictionaryWithObjectsAndKeys:@"www.99ios.com",@"website",@"九九学院",@"name", nil];
NSLog(@"第二种类型创建的字典为:%@",dict1);
方式三:创建一个key的数组,创建一个value的数组,打印顺序也是从后往前
NSArray *keys = @[@"website",@"name"];
NSArray *values = @[@"www.99ios.com",@"九九学院"];
NSDictionary *dict2 = [NSDictionary dictionaryWithObjects:values forKeys:keys];
NSLog(@"第三种类型创建的字典为:%@",dict2);
//访问键值的两种方式
NSString *website = dict[@"website"];
NSLog(@"website的值是:%@",website);
NSString *name = [dict objectForKey:@"name"];
NSLog(@"name的值是:%@",name);
NSNumber *num = dict[@"founderYear"];
NSLog(@"founderYear的值是%@",num);
//遍历字典中的键值对
for (NSDictionary *key in dict) {
NSLog(@"key:%@,value:%@",key,dict[key]);
}
打印的结果为:
//获取键值对的数量
NSUInteger count = dict.count;
NSLog(@"dict中的键值对数量为:%lu",(unsigned long)count);
打印的结果为:
获取字典中所有的键:
//获取一个字典中所有的键
NSArray *allKeyArrays = dict.allKeys;
NSLog(@"dict中所有的键为:%@",allKeyArrays);
打印结果为:
获取字典中所有的值:
//获取一个字典中所有的值
NSArray *allValuesArrays = dict.allValues;
NSLog(@"dict中所有的值为:%@",allValuesArrays);
打印结果为:
可变字典(NSMutableDictionary)
可变字典是不可变字典的子类,NSMutableDictionary继承了NSDictionary类的属性和方法,其键值对可以增加、修改和删除
//创建可变字典的3种方式
NSMutableDictionary *mutableDict = [NSMutableDictionary dictionary];
NSMutableDictionary *mutableDict1 = [NSMutableDictionary dictionaryWithCapacity:100];
NSMutableDictionary *mutableDict2 = [NSMutableDictionary dictionary];
[mutableDict2 initWithContentsOfFile:@"path"];
增加键值对:
//增加键值对
[mutableDict setObject:@"www.99ios.com" forKey:@"website"];
[mutableDict setObject:@"九九学院" forKey:@"name"];
NSLog(@"website:%@",mutableDict[@"website"]);
NSLog(@"name:%@",mutableDict[@"name"]);
打印结果为:
//修改键值对的值
mutableDict[@"website"] = @"www.apple.com";
mutableDict[@"name"] = @"苹果公司";
NSLog(@"新的website的值为:%@",mutableDict[@"website"]);
NSLog(@"新的name的值为:%@",mutableDict[@"name"]);
//移除键值对中的值
[mutableDict removeObjectForKey:@"website"];
NSLog(@"移除website之后的可变字典为:%@",mutableDict);
打印结果为:
//移除多个键值对,把移除的所有的键存放在一个数组中
NSArray *removeDicts = @[@"website",@"name"];
[mutableDict removeObjectsForKeys:removeDicts];
NSLog(@"移除website和name之后的可变字典为:%@",mutableDict);
打印结果为:
移除所有键值对:
//移除所有键值对
[mutableDict removeAllObjects];
NSLog(@"移除所有键值对后的字典为:%@",mutableDict);
打印结果为:
二、分类
1.Category的实现原理,以及Category为什么只能加方法不能加属性
2.Category中有load方法吗?load方法是什么时候调用的?load方法能继承吗?
3.load、initialize在category中调用的顺序,以及出现继承时他们之间的调用过程?
4.load、initalize的区别,以及他们在category重写的时候调用的次序?
三、对象的本质
1.一个NSObject对象占用多少内存?
答:系统分配了16个字节给NSObject,但NSObject对象只使用了8个字节(64位环境)
@interface Person : NSObject
{
@public
int _age;
}
@end
@implementation Person
@end
@interface Student : Person
{
int _no;
}
@end
@implementation Student
@end
我们创建出来的实例对象,他的内存里面不存在方法,只存有成员变量和isa指针,为什么实例对象里面不包含方法,因为每一个实例对象都可以为他的属性赋自己固定的值,但是方法只需要都是通用的,只需要调用1次
@interface Person : NSObject
{
@public
int _age;
}
@property (nonatomic, assign) int height;
@end
定义一个属性的本质是添加一个_的成员变量,同时生成get和set方法
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _height;
};
// @property (nonatomic, assign) int height;
/* @end */
// @implementation Person
static int _I_Person_height(Person * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_Person$_height)); }
static void _I_Person_setHeight_(Person * self, SEL _cmd, int height) { (*(int *)((char *)self + OBJC_IVAR_$_Person$_height)) = height; }
2.对象的isa指针指向哪里?
1)OC对象的分类
OC中的对象主要分为3种
instance对象就是通过类alloc出来的对象,每次调用alloc产生新的instance对象
NSObject *object1 = [[NSObject alloc]init];
NSObject *object2 = [[NSObject alloc]init];
object1、object2是NSObject的instance对象(实例对象)
instance对象在内存中存储的信息包括:isa指针和成员变量的值
//实例对象
MJPerson *person = [[MJPerson alloc]init];
person->_age = 10;
MJPerson *person2 = [[MJPerson alloc]init];
person2 -> _age = 20;
NSLog(@"person对象的地址是:%p,person2对象的地址是:%p",person,person2);
b.class对象 (类对象)
一个类的类对象是唯一的,一个类的类对象在内存中只有一份,有3种方式可以得到类对象,将他们的地址打印是同一个类对象
//类对象
// 1)通过实例对象调用class方法 一个类的类对象在内存中只有一份,一个类的类对象是唯一的
Class personClass = [person class];
Class personCLass3 = [person2 class];
//2)通过类直接调用class方法
Class personClass1 = [MJPerson class];
//3)通过object_getClass方法
Class personClass2 = object_getClass(person);
Class personClass4 = object_getClass(person2);
NSLog(@"MJPerson的类对象的地址是:%p %p %p %p %p",personClass,personCLass3,personClass1,personClass2,personClass4);
isa指针 superclass指针 类的属性信息(@property) 类的对象方法信息(instance method)
类的协议信息(protocal) 类的成员变量信息(ivar) (指的是类型和名称,不是值)
元类对象 将类对象传入获取到元类对象 需要注意的是获取元类对象不能使用类对象调用class放大,class方法始终获取到的都是类对象,因为类对象只有1个,所以传入类对象进去,获取到的元类对象也只有1个
meta-class对象和class对象的内存结构是一样的,但是用途是不一样的,在内存中存储的信息主要包括:
isa指针 superclass指针 类的类方法信息(class method)
Class personMetaClass = object_getClass([MJPerson class]);
Class personMetaClass1 = object_getClass(personClass);
NSLog(@"personMetaClass的内存地址是:%p personMetaClass1的内存地址是%p",personMetaClass,personMetaClass1);
//实例对象
MJPerson *person = [[MJPerson alloc]init];
//调用实例方法
[person personInstanceMethod];
//这个方法调用的本质是
// objc_msgSend(person,@selector(personInstanceMethod));
// objc_msgSend();
//调用类方法
[MJPerson personClassMethod];
//类方法调用的本质是
// objc_msgSend(MJPerson,@selector(personClassMethod));
当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用
当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现并调用
3.OC的类信息存放在哪里?
四、KVO
1.ios用什么方式实现对一个对象的KVO?(KVO的本质是什么,KVO的内部实现原理)
答:当一个对象使用了KVO监听,ios系统会修改这个对象的isa指针,改为指向一个全新的通过Runtime动态创建的子类,子类拥有自己的set方法实现,内部会调用willChangeValueForKey,原来的set方法和didChangeValueForKey方法,didChangeValueForKey这个方法内部会调用监听器的监听方法
1)KVO全称是Key-Value Observing ,俗称“键值监听“,可以用于监听某个对象属性值的改变
#import "ViewController.h"
#import "MJPerson.h"
#import "MJStudent.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
MJPerson *p1 = [[MJPerson alloc]init];
p1.age = 1;
MJPerson *p2 = [[MJPerson alloc]init];
p2.age = 2;
p1.age = 10;
NSKeyValueObservingOptions options= NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
[p2 addObserver:self forKeyPath:@"age" options:options context:@"456"];
p1.age = 10;
p2.age = 20;
MJStudent *student = [[MJStudent alloc]init];
[student changeAge];
[p1 removeObserver:self forKeyPath:@"age"];
[p2 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到%@的%@属性值改变了----%@",object,keyPath,change);
}
@end
MJPerson类
.h文件
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface MJPerson : NSObject
@property (nonatomic, assign) int age;
@end
NS_ASSUME_NONNULL_END
.m文件
#import "MJPerson.h"
@implementation MJPerson
@end
MJStudent
.h文件
#import <Foundation/Foundation.h>
#import "MJPerson.h"
NS_ASSUME_NONNULL_BEGIN
@interface MJStudent : NSObject
@property (nonatomic, strong) MJPerson *person;
- (void)changeAge;
@end
NS_ASSUME_NONNULL_END
.m文件
#import "MJStudent.h"
@implementation MJStudent
- (void)changeAge {
self.person = [[MJPerson alloc]init];
self.person.age = 30;
}
@end
打印的结果为:
需要注意的是监听某个对象的属性的值,像上面在MJStudent里面创建了一个MJPerson的对象,他和p1和p2对象是不同的对象,所以监听不到他的属性值的改变。还有就是需要在改变他的值之前给他添加监听,如果要是在修改之后监听的话也监听不到他的属性值的改变。
2)KVO的本质
person1.age = 10; //[person1 setAge:10];
@interface MJPerson : NSObject
@property (nonatomic ,assign) int age;
@end
@implementation MJPerson
- (void)setAge:(int)age {
_age = age;
}
@end
实例对象的isa指针指向类对象,当person1的属性值还没有发生改变的时候,会发现它的isa指针指向他的类对象,当他的isa指针发生改变的时候,会指向一个新的类对象,这个类对象是NSKVONotifying_MJPerson
会直接调用MJPerson的setAge方法
使用了KVO监听的对象,会在运行时产生一个新的类 NSKVONotifying_MJPerson,他是MJPerson的一个子类
- (void)setAge:(int)age {
__NSSetIntValueAndNotify();
}
void __NSSetIntValueAndNotify(......) {
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key {
[observer observeValueForKeyPath:@"age" ofObject:self change:@{} context:nil];
}
使用了KVO监听的对象,系统会在运行时产生一个新的类,这个类有一个setAge的方法,setAge方法会调用C语言的__NSSetIntValueAndNotify()的方法,这个方法里面会调用willChangeValueForKey方法,然后调用父类的setAge方法,然后调用didChangeValue方法,didChangeValue方法里面调用了observeValueForKeyPath的方法,所以能够监听到他的值的变化
3)验证isa指针和IMP实现
NSLog(@"p1监听KVO之前的--p1的Class%@ --p2的Class%@",object_getClass(person1),object_getClass(person2));
NSLog(@"p1监听KVO之后的--p1的Class%@ --p2的Class%@",object_getClass(person1),object_getClass(person2));
NSLog(@"p1监听KVO之前的-----%p,%p",[person1 methodForSelector:@selector(setAge:)],[person2 methodForSelector:@selector(setAge:)]);
NSLog(@"p1监听KVO之后的-----%p,%p",[person1 methodForSelector:@selector(setAge:)],[person2 methodForSelector:@selector(setAge:)]);
//
// MJPerson.m
// KVO的基本使用
//
// Created by Hanvon on 2023/2/16.
//
#import "MJPerson.h"
@implementation MJPerson
- (void)setAge:(int)age {
NSLog(@"调用setAge方法");
_age = age;
}
- (void)willChangeValueForKey:(NSString *)key {
NSLog(@"willChangeValue--begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValue--end");
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"didChangeValue--begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValue--end");
}
//- (int)age {
// return _age;
//}
@end
会发现确实在didChangeValueForKey方法里面进行的observer方法的调用
2.如何手动触发KVO(如何手动触发一个value的KVO,如何关闭默认的KVO实现,并进入自定义的KVO实现)
答:手动调用willChangeValueForKey和didChangeValueForKey方法
[p1 willChangeValueForKey:@"age"];
[p1 didChangeValueForKey:@"age"];
五、性能优化
1.你在项目中是怎么优化内存的?
CPU(Center Processing Unit,中央处理器),对象的创建和销毁,对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码,图像的绘制
CPU(Graphics Processing Unit,图像处理器)纹理的渲染
2)图像的成像原理
会先发一个垂直同步信号,然后这一屏幕上发水平同步信号,等到一屏幕铺满,再发垂直同步信号
当CPU计算完毕之后,GPU进行渲染,然后发送垂直同步信号,如果GPU渲染时间过长,垂直信号就会将上一帧的内容进行显示,俗称掉帧,这一帧的内容就会在下一次垂直信号发出的时候显示,就造成了卡顿
b.按照60FPS的帧刷新率,每隔16ms就会有一次VSync信号发出。
c.平时所说的卡顿主要是因为在主线程中执行了比较耗时的操作,可以通过Observer到主线程RunLoop中,通过监听RunLoop切换的耗时,以达到监控卡顿的目的
a.尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView
b.不要频繁地调用UIView的属性,比如frame、bounds、transform等属性,尽量减少不必要的修改
c.尽量提前计算好布局,在有需要时一次性的调整对应的属性,不要多次的修改属性
self.view.frame = CGRectMake(self.view.frame.origin.x+1,self.view.frame.origin.y,self.view.frame.size.width,self.view.frame.size.height);
d.Autolayout会比直接设置frame消耗更多的CPU资源
e.图片的size最好刚好跟UIImageView的size保持一致
g.尽量把耗时的操作放到子线程(文本处理(尺寸计算、绘制),图片处理(解码、绘制))
//文本计算
[@"text" boundingRectWithSize:CGSizeMake(100, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:nil context:nil];
//文本绘制
[@"text" drawWithRect:CGRectMake(0, 0, 100, 200) options:NSStringDrawingUsesLineFragmentOrigin attributes:nil context:nil];
6)GPU方面优化卡顿
a.尽量避免短时间内大量图片展示,尽可能将多张图片合成一张进行显示
b.GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用GPU资源进行处理,所以纹理尽量不要超过这个尺寸
c.尽量减少视图数量和层次
d.减少透明的视图(alpha<1),不透明的设置成opaque为YES
7)离屏渲染
a.在OpenGL中,GPU有两种渲染方式:
On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作
Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
需要创建新的缓冲区
离屏渲染的整个过程中,需要多次切换上下文,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),等到离屏渲染结束以后,将离屏缓冲区的渲染结果渲染到屏幕上,又需要将上下文环境从离屏切换到当前屏幕
光栅化:layer.shouldRasterize = YES
圆角:同时设置layer.masksToBounds = YES layer.cornerRadius = 0;(考虑通过CoreGraphics绘制裁剪圆角,或者叫美工提供圆角图片)
阴影:layer.shadowXXX(如果设置了layer.shadowPath就不会产生离屏渲染)
(二)耗电优化
1)耗电的主要来源
耗电的主要来源是CPU处理(Processing)、网络(NetWorking)、定位(Location)和图像(Graphics)
2)耗电优化:
CPU、GPU方面优化:
a.尽可能降低CPU、GPU的功耗
b.少用定时器
c.优化I/O操作
读写大量重要数据时,考虑用dispatch_io,其提供了基于GCD的同步操作文件I/O的API,用dispatch_io系统会优化磁盘访问
数据量比较大的,建议使用数据库(比如SQLite,CoreData)
网络优化:
e.让用户可以取消长时间运行或者速度很慢的网络操作,设置合理的超时时间
f.批量传输,比如,下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块的下载,如果下载广告,一次性多下载一些,然后再慢慢提示,如果下载电子邮件,一次下载多封,不要一封一封的下载
定位优化:
a.如果只是需要快速确定用户位置,最好用CLLocationManager的requestLocation方法,定位完成以后,会自动让定位硬件断电
b.如果不是导航应用,尽量不要实时更新位置,定位完毕就关闭定位服务
c.尽量降低定位精度,比如尽量不要使用精度最高的kCLLocationAccuracyBest
d.需要后台定位时,尽量设置pausesLocationUpdatesAutomatically为YES,如果用户不太可能移动的时候系统会自动暂停位置更新
热启动(Warm Launch):App已经存在在内存中,在后台存活着,再次点击图标启动App
2)App启动时间的优化,主要是针对冷启动进行优化
通过添加环境变量可以打印出App的启动时间分析(Edit scheme ->run -> Arguments)
DYLD_PRINT_STATISTICS设置为1
2.优化你是从哪几方面着手的?
3.列表卡顿的原因有哪些?你平时是怎么优化的?
4.遇到tableView卡顿嘛?会造成卡顿的原因大致有哪些?
六、多线程
1.你理解的多线程?
2.ios的多线程方案有哪几种?你更倾向于哪一种?
3.你在项目中使用过GCD吗?
1)GCD的常用函数
dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_sync(queue, ^{
NSLog(@"执行任务~%@",[NSThread currentThread]);
});
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"执行任务~%@",[NSThread currentThread]);
});
2)GCD的队列可以分为2大类型
并发队列:
可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"执行任务1~%@",[NSThread currentThread]);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"执行任务2~%@",[NSThread currentThread]);
}
});
串行队列:
让任务一个接着一个的执行(一个任务执行完毕以后,再执行下一个任务)
//串行队列
dispatch_queue_t queue1 = dispatch_queue_create("muqueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue1, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"执行任务1~%@",[NSThread currentThread]);
}
});
dispatch_async(queue1, ^{
for (int i = 0; i < 10; i++) {
NSLog(@"执行任务2~%@",[NSThread currentThread]);
}
});
会按顺序执行,执行完任务1,在执行任务2
所以只要是同步函数,只会在当前线程里面执行任务,就是按顺序来执行的
同步和异步主要影响:能不能开启新的线程
同步:在当前线程中执行任务,不具备开启新线程的能力
异步:在新的线程中执行任务,具备开启新线程的能力
串行:一个任务执行完毕以后,再执行下一个任务
异步只是具备了开启新线程的能力,但是不一定开新的线程
dispatch_queue_t mainQueue = dispatch_get_main_queue();
//主队列
dispatch_async(mainQueue, ^{
NSLog(@"主线程执行任务3~%@",[NSThread currentThread]);
});
主队列是一个特殊的串行队列
4)各种队列的执行效果
5)面试题
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"执行任务1");
[self interview01];
NSLog(@"执行任务3");
}
- (void)interview01 {
//以下代码是在主线程执行的,会不会产生死锁 会
//队列:排队:FIFO
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"执行任务2");
});
}
队列的特点是:FIFO,先进先出
dispatch_sync:立马在当前线程执行任务,执行完毕才能继续往下执行
队列是先进先出,会先执行viewDidLoad里面的任务, 所以会先执行任务1,当执行到sync函数的时候,会立即执行sync里面的任务,但是队列先进先出,viewDidLoad没有执行完毕,任务2要执行,就产生了死锁
b.异步主队列会不会产生死锁(不会)
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"执行任务1");
[self interview02];
NSLog(@"执行任务3");
}
- (void)interview02 {
//以下代码是在主线程执行的,会不会产生死锁 会
//队列:排队:FIFO
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"执行任务2");
});
}
dispatch_async不要求马上在当前线程同步执行任务
所以会先执行viewDidLoad里面的任务,执行完毕以后在执行async任务里面的方法
c.以下代码会不会产生死锁(会)
- (void)viewDidLoad {
[super viewDidLoad];
[self interview03];
}
- (void)interview03 {
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"执行任务2");
dispatch_sync(queue, ^{
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
串行队列,会先执行viewDidLoad里面的任务,然后在执行async里面的,虽然async会创建一个新的线程,但是是同步执行,所以还是先主线程执行完viewDidLoad里面的任务,在执行async里面的任务,但是block0执行完任务2,再执行任务3的时候的前提是执行完block0,但是sync又需要马上执行,就会产生死锁
d.以下代码会不会产生死锁(不会)
- (void)interview04 {
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("myqueue2", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"执行任务2");
dispatch_sync(queue2, ^{
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
这个会创建一个并发的队列,sync又要求立马执行,所以可以执行完任务2,在执行并发队列的任务3,在执行任务4
e. 以下代码会不会产生死锁 (不会)
- (void)interview05 {
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"执行任务2");
dispatch_sync(queue, ^{
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)
6)全局队列的地址是一个,从始至终用的都是一个队列,创建的并发队列,每创建一个都是不同的队列
dispatch_queue_t queue1 = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue2 = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue3 = dispatch_queue_create("queue3", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue4 = dispatch_queue_create("queue4", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"%p, %p ,%p %p",queue1,queue2,queue3,queue4);
可以看出来全局的是一个地址,创建的是不同的地址
7)performSelector方法
a.
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(@"1");
//几秒之后再执行这个test方法
[self performSelector:@selector(test) withObject:nil afterDelay:3.0];
}
- (void)test {
NSLog(@"2");
}
performSelector withObject afterDelay方法的作用是在几秒后执行@selector方法
b.
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(@"1");
//几秒之后再执行这个test方法
[self performSelector:@selector(test) withObject:nil afterDelay:0.0];
NSLog(@"3");
}
- (void)test {
NSLog(@"2");
}
performSelector withObject afterDelay这个方法是在Runloop里面添加了计时器,即使延迟0.0秒进行执行,也是会先执行下面的函数,函数在执行selector函数
c.
- (void)test {
NSLog(@"2");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
// [self performSelector:@selector(test) withObject:nil afterDelay:0.0];
[self performSelector:@selector(test) withObject:nil];
NSLog(@"3");
});
}
performSelector withObject函数的本质就是使用msgSend函数,所以正常执行
d.
- (void)test {
NSLog(@"2");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:0.0];
NSLog(@"3");
});
}
这个方法中test2就不执行了
原因就是
performSelector withObject afterDelay的本质就是往RunLoop中添加定时器,子线程默认是不启动RunLoop的,所以没有RunLoop,这个代码不执行
如果想让这个代码执行的话,就是往子线程中添加RunLoop
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:0.0];
NSLog(@"3");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});
}
这样的话test方法就可以执行了
8)GNUstep
GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍
源码地址:http://www.gnustep.org/resources/downloads.php
虽然GNUstep不是苹果的官方源码,但还是具有一定的参考价值
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_sync(queue, ^{
[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"123");
}];
});
在子线程当中不能执行NSTimer的操作,因为开启了一个子线程,就要执行block操作,执行完毕之后子线程就关闭了,3秒之后任务就不能执行了
9)面试题
NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSLog(@"1");
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
- (void)test {
NSLog(@"2");
}
这个结果就是崩了,NSThread开启了一个子线程,然后子线程执行block里面的任务,主线程仍然往下执行,performSelector函数,执行test方法,但是在block执行完毕以后,子线程就结束了,所以执行不了test方法,就崩了,解决方案就是在子线程里面添加RunLoop,阻止子线程被杀掉
NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSLog(@"1");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
4.GCD的队列类型
5.说一下OperationQueue和GCD的区别,以及各自的优势?
6.线程安全的处理手段有哪些?
7.OC你了解的锁有哪些?在你回答的基础上进行二次提问:
追问一:自旋和互斥对比?
追问二:使用以上锁需要注意哪些?
追问三:用C/C++/OC,任选其一,实现自旋与互斥,口述即可
七、苹果支付
1.ios支付主要分为两种,第三方支付和应用内支付(内购)
第三方支付:包括微信支付、支付宝支付、银联支付、百度钱包、京东支付等
应用内支付:在应用程序内购买虚拟商品,将收到支付金额的百分之七十
2.第三方支付
第三方支付的弹出方式有两种,网页和调用app,有些第三方支付没有安装客户端,可以直接弹出网页进行支付,手机中安装了客户端可以跳转到app进行支付,微信支付只能调用app进行支付
3.支付宝支付
https://b.alipay.com/page/portal/home
4)支付流程
a.在商户服务平台先与支付宝签约,获得商户ID(partner)和账号ID(seller),需要提供公司资质或者营业执照,个人无法申请。
c.下载支付宝SDK:
e.调用支付宝客户端,由支付宝客户端跟支付宝安全服务器打交道
一、ios内存管理之野指针
1.野指针和空指针的概念
C语言中的野指针:声明一个指针变量,但是没有赋初始值,此时指针指向一个垃圾值,即指向一块随机的内存空间
OC中的野指针:指针所指向的对象已经被释放/回收了,但是指针没有作任何的修改,仍然指向已经回收的内存空间。
空指针:没有指向任何东西的指针,即nil、NULL、0,向空指针发送消息不会报错。
C中野指针事例: 此时没有对str2进行赋值,所以打印出来会是一个随机值,而且程序报错
OC中野指针事例:创建一个Person类的对象,创建完成之后将她release掉,此时的p就变成了野指针,需要注意的是虽然是野指针,但是不会报上述的错误,如果用对象调用其方法,也是可以执行。(是因为此时对象中的数据还可能在内存中)
(1)僵尸对象:是一种用来检查内存错误(EXC_BAD_ACCESS)的对象,它可以捕获任何尝试访问坏内存的调用
(2)如果给僵尸对象发送消息,那么将在运行期间崩溃并输出错误日志,可以通过日志定位到野指针对象调用的方法和类名
(3)通俗来讲:僵尸对象就是指一个引用计数器为0、被释放的OC对象,此时这个对象的内存已经被系统回收,该对象可能还存在,即数据依然在内存中,但是此时僵尸对象已经不稳定了,其内存可能随时被别的对象申请占用,所以此时僵尸对象是不可以再访问和使用的。
3.内存回收的本质
(1)申请一块内存空间,其本质是向系统申请一块别人不再使用的内存空间,即已经回收的内存空间。
(2)释放一块内存空间,其本质是这个内存空间不再使用,可以由系统分配给别的对象使用,此时内存空间虽然回收了,但是原本的数据依赖是存在的,可以理解为垃圾数据,所以内存回收可以理解为以下两点
@1 OC对象释放后,内存回收,表示这一块内存可以分配给别的对象了;
@2 这块内存在分配给别的对象之前,仍然保留着已经释放对象的数据。
5.检测野指针,
在OC中野指针错误不是必现的,比如上面的例子,我们可以开始僵尸对象检测机制
然后我们开启僵尸对象检测工具,选择Edit Scheme进入到下面的选项框,然后勾选检测僵尸对象按钮。
6. 将Person类变成了NSZombie类的底层实现分析。
(1)打开Instruments工具,选择zombie对象检测
(2)进入到检测页面,然后进行执行,将选择栏选到call tree下面,点击main函数进行分析,可以看到release方法其实是实现了父类的release方法
(3)可以看到底层调用了__dealloc_zombie方法,我们可以点击进去,直接复制这个方法
答:内存管理的主要作用是用来存储数据的,通过声明一个变量,可以将数据存储进去,
内存主要分为5大区域:栈、堆、BSS段、数据段和代码段,栈主要用来存储局部变量,当局部变量的作用域执行完毕之后,这个局部变量就会被回收掉,堆主要用来存储OC对象,BSS段主要用来存储未初始化的全局变量和静态变量,当全局变量和静态变量被初始化之后,就会被回收,并且存储到数据段之中,数据段主要用来存储初始化之后的全局变量和静态变量,当程序结束的时候被回收,代码段主要用来存储代码,当程序结束的时候被回收,存储在栈、BSS段、数据段和代码段中的数据系统会自动回收,所以内存管理我们只需要对存储在堆上的数据进行管理,内存管理的分类主要分为ARC和MRC,每一个对象都有一个retainCoun属性,用来记录这个对象有多少个人在使用。MRC是手动引用计数,遵循饿原则是谁创建谁销毁,当创建出来一个对象,他的引用计数器默认是1,(此时需要注意不是所有的对象创建出来以后引用计数器都是1,比如NSNumber,NSNumber一般创建的是自动释放的对象,自动释放的对象的retainCount也是不可靠的,NSString创建常量区对象时,他的retainCount默认也不是1)我们可以通过向这个对象发送retain消息来使他的引用计数器加1,通过发送release消息使他的引用计数器减1,直到他的引用计数器变为0,系统自动回收,并自动调用对象的delloc方法。ARC是自动引用计数,对象的计数管理完全由系统来管理,ARC的实现原理主要是在程序预编译阶段,将ARC的代码转换为非ARC的代码,自动加入retain、release、autorelease方法。内存管理还有自动释放池,自动释放池的原理就是将存入到自动释放池中的对象,在自动释放池被销毁的时候,会自动调用存在在自动释放池中的所有对象的release方法,我们可以通过autorelease方法将一个对象存储在自动释放池中,他仅仅是为对象发送一条release方法,并不是对对象进行了销毁。如果不能很好的对内存进行管理,就会造成内存泄露。
对NSNumber对象和NSString对象的默认引用计数验证:
二、内存问题
1.使用CADisplayLink,NSTimer有什么注意点?
答:CADisplayLink,NSTimer会对target产生强引用,如果target又对他们产生强引用,就会引发循环引用。
存在的问题:
1)我们创建一个CADisplayLink对象,希望在页面销毁的时候计时器销毁,页面也进行销毁
在viewDidLode中添加如下代码
//保证调用频率和屏幕的刷帧频率,60FPS,一秒钟调用60次
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
然后实现方法
- (void)linkTest {
NSLog(@"%s",__func__);
}
页面销毁的方法,dealloc方法,注意页面销毁的delloc方法的调用,是当它back返回到上一个页面的时候才会调用
- (void)dealloc {
[self.link invalidate];
}
此时运行项目会发现dealloc方法并没有执行,计时器也没有关闭,因为他们循环引用都没有销毁
2)NSTimer的使用也存在相应的问题
在viewDidLoad里面创建一个NSTimer的对象
- (void)viewDidLoad {
[super viewDidLoad];
// __weak typeof(self) weakSelf = self;
// __weak ViewController *weakSelf1 = self;
// self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:weakSelf selector:@selector(timeTest) userInfo:nil repeats:YES];
}
执行方法
- (void)timeTest {
NSLog(@"%s",__func__);
}
在页面销毁的时候,计时器销毁
- (void)dealloc {
[self.timer invalidate];
}
思考:能否用__weak来修饰self避免循环引用?答案是不能,因为NSTimer本身对target进行了循环引用,我们改成weakSelf也只是传过去一个值,并不能修改NSTimer里面的强引用
解决方案:
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timeTest];
}]
解决方案二:创建一个中间量target,让他弱引用viewController。
创建一个类继承自NSObject
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LJProxy : NSObject
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
NS_ASSUME_NONNULL_END
#import "LJProxy.h"
@implementation LJProxy
+ (instancetype)proxyWithTarget:(id)target {
LJProxy *proxy = [[LJProxy alloc]init];
proxy.target = target;
return proxy;
}
//消息转发机制
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}
@end
在创建NSTimer对象的时候让target调用这个方法
//解决方案二:
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[LJProxy proxyWithTarget:self] selector:@selector(timeTest) userInfo:nil repeats:YES];
此时就解决了这个问题
2.autorelease在什么时机会被释放?
四、分类和延展的区别
1.如何创建一个延展
2)选择Objective-C File
2.延展的介绍
2)特殊之处:
3.延展的语法
@end
在本类中添加延展的头文件,在调用本类和延展的方法和属性的类中导入延展的头文件
#import <Foundation/Foundation.h>
#import "Person+itcast.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
Person *person = [[Person alloc]init];
[person run];
[person sayHi];
[person sleep];
}
return 0;
}
4.延展的基本使用
1)延展的本质是1个分类,作为本类的一部分,只不过是1个特殊的分类,没有名字
5.延展和分类的区别
2)每1个分类都有单独的声明和实现,而延展只有声明,没有单独的实现,和本类共享1个实现
3)分类只能新增方法,而延展当中任意的成员都可以添加,属性和方法
4)分类当中可以写@property,但是只会生成getter和setter的声明, 延展当中写@property会自动生成私有属性,也会生成getter和setter的声明和实现
6.延展的应用场景
原文地址:https://blog.csdn.net/qq_43658148/article/details/125088140
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_36570.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!