本文介绍: 本篇主要讲解ios中的有关内存问题,会不断更新,有问题欢迎指出和提问哦!

本篇主要针对面试题进行解析,会进行基础知识总结和拓展,仅供参考,如有错误,欢迎指出,一起学习

一、关于Foundation框架中的问题

(一)NSCache & NSDictionary

1.NSDictionary(字典

字典是由键-值(keyvalue)组成的数据集合,其中值为对象可以通过键从字典获取需要的值。字典中的键必须唯一,通常情况下,键为字符串对象,主要用于注明存储对象说明,但键也可以是其他类型的对象,和键相关联的值可以是任何类型的对象,但是不能是nil

关于字典思维导图

​​​​​​​ 

可变字典(NSDictionary)

可变字典一旦创建好之后,不能在增加/删除/修改键值

创建可变字典几种方式

方式一:打印结果不一定按顺序排列

    NSDictionary *dict = @{
        @"website":@"www.99ios.com",
        @"name":@"九九学院",
        @"business":@"ios学习",
        @"founderYear":@2016,
    };
    NSLog(@"第一种类型创建字典为:%@",dict);

打印出来的结果为:

方式二:注意其顺序,是valuekey,并且以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);

 打印结果

从字典中获取键值

 使用dict[key]或者是objectForKey

    //访问键值的两种方式
    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);

打印结果为:

移除多个键值对,需要移除key存放在一个数组中

    //移除多个键值对,把移除的所有的键存放在一个数组中
    NSArray *removeDicts = @[@"website",@"name"];
    [mutableDict removeObjectsForKeys:removeDicts];
    NSLog(@"移除website和name之后的可变字典为:%@",mutableDict);

 打印结果为:

   

 移除所有键值对:

    //移除所有键值对
    [mutableDict removeAllObjects];
    NSLog(@"移除所有键值对后的字典为:%@",mutableDict);

 打印结果为:

二、分类 

1.Category的实现原理,以及Category为什么只能加方法不能加属性

2.Category中有load方法吗?load方法什么时候调用的?load方法能继承吗?

3.loadinitialize在category调用的顺序,以及出现继承他们之间调用过程

4.loadinitalize的区别,以及他们category重写时候调用的次序?

三、对象的本质

1.一个NSObject对象占用多少内存​​​​​​​

答:系统分配了16个字节给NSObject,但NSObject对象只使用了8个字节(64位环境​​​​​​​

9. 复杂继承内存空间占用

@interface Person : NSObject

{
    @public
    int _age;
}
@end

@implementation Person

@end
@interface Student : Person
{
    int _no;
}

@end

@implementation Student


@end

内存对齐原则

结构体的大小必须是最大成员大小倍数 

10.定义属性的本质

我们创建出来的实例对象,他的内存里面存在方法,只存有成员变量和isa指针为什么实例对象里面包含方法,因为每一个实例对象都可以为他的属性赋自己固定的值,但是方法只需要都是通用的,只需要调用1次

@interface Person : NSObject

{
    @public
    int _age;
}

@property (nonatomic, assign) int height;

@end

定义一个属性的本质是添加一个_的成员变量,同时生成getset方法

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种

a.instance对象(实例对象)

instance对象就是通过alloc出来的对象,每次调用alloc产生新的instance对象

    NSObject *object1 = [[NSObject alloc]init];
    NSObject *object2 = [[NSObject alloc]init];

object1、object2是NSObject的instance对象(实例对象)

他们不同两个对象,分别占据着两块不同内容

instance对象在内存存储信息包括:isa指针成员变量的值

isa指针内存地址就是Person对象的内存地址

    //实例对象
    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);

class对象在内存中存储的主要信息包括:

isa指针    superclass指针  类的属性信息(@property) 类的对象方法信息(instance method

类的协议信息(protocal)         类的成员变量信息(ivar) (指的是类型和名称,不是值)

c.meta-class对象 (元类对象)

元类对象  将类对象传入获取到元类对象 需要注意的是获取元类对象不能使用类对象调用class放大,class方法始终获取到的都是类对象,因为类对象只有1个,所以传入类对象进去,获取到的元类对象也只有1个

每个类在内存中有且只有一个meta-class对象

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);


​​​​​​​

 2)isa指针指向哪里?

    //实例对象
    MJPerson *person = [[MJPerson alloc]init];
    
    //调用实例方法
    [person personInstanceMethod];
    
    //这个方法调用的本质是
//    objc_msgSend(person,@selector(personInstanceMethod));
//    objc_msgSend();
    
    //调用类方法
    [MJPerson personClassMethod];
    //类方法调用的本质是
//    objc_msgSend(MJPerson,@selector(personClassMethod));

调用对象方法的本质是向实例对象发送一个消息去调用对象方法

调用类方法的本质是向类对象发送一个消息去调用类方法

实例对象的isa指向类对象,类对象的isa指向元类对象

当调用对象方法时,通过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 ,俗称“键值监听“,可以用于监听某个对象属性值的改变

ViewController类


#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的本质

给属性进行赋值实际上就是调用了该对象的set方法

    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

没有使用KVO监听的对象

直接调用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实现

a.可以使用系统runtime函数去获取到类对象

  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));

 

 b.打印出对象的方法的地址,然后控制台看他的方法实现

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:)]);

​​​​​​​

4)验证KVO底层原理的实现

MJPerson类里面添加如下的3个方法

//
//  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.你在项目中是怎么优化内存的?

(一)卡顿优化

1)在屏幕成像的过程中,CPU和GPU起着至关重要作用

CPU(Center Processing Unit,中央处理器),对象的创建和销毁,对象属性的调整、布局计算文本计算排版图片格式转换解码图像绘制

CPU(Graphics Processing Unit,图像处理器)纹理渲染

 

ios中是双缓冲机制,有前帧缓存、后帧缓存

2)图像的成像原理

会先发一个垂直同步信号,然后这一屏幕上发水平同步信号,等到一屏幕铺满,再发垂直同步信号

3)造成卡顿原因

当CPU计算完毕之后,GPU进行渲染,然后发送垂直同步信号,如果GPU渲染时间过长,垂直信号就会将上一帧的内容进行显示,俗称掉帧,这一帧的内容就会在下一次垂直信号发出的时候显示,就造成了卡顿

4)卡顿解决的主要思路

a.尽可能减少CPU、GPU资源消耗

b.按照60FPS的帧刷新率,每隔16ms就会有一次VSync信号发出。

c.平时所说的卡顿主要是因为在主线程中执行比较耗时的操作可以通过Observer到主线程RunLoop中,通过监听RunLoop切换的耗时,以达到监控卡顿的目的

5)CPU方面优化卡顿

a.尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView

b.不要频繁地调用UIView的属性,比如frame、boundstransform等属性,尽量减少不必要的修改

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保持一致

f.控制一下线程最大并发数量

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

e.尽量避免减少出现离屏渲染

7)离屏渲染

a.在OpenGL中,GPU有两种渲染方式:

On-Screen Rendering:当前屏幕渲染,在当前用于显示屏幕缓冲区进行渲染操作

Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作

b.离屏渲染消耗性能原因

需要创建新的缓冲

离屏渲染的整个过程中,需要多次切换上下文,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),等到离屏渲染结束以后,将离屏缓冲区的渲染结果渲染到屏幕上,又需要将上下文环境从离屏切换到当前屏幕

c.哪些操作会触发离屏渲染?

光栅化:layer.shouldRasterize = YES

遮罩layer.mask

圆角:同时设置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)

网络优化:

a.减少、压缩网络质量

b.如果多次请求的结果是相同的、尽量使用缓存

c.使用断点续传,否则网络不稳定时可能多次传输相同内容

d.网络不可用时,不要尝试执行网络请求

e.让用户可以取消时间运行或者速度很慢的网络操作设置合理超时时间

f.批量传输比如下载视频流时,不要传输很小的数据包直接下载整个文件或者一大块一大块的下载,如果下载广告,一次性多下载一些,然后再慢慢提示,如果下载电子邮件,一次下载多封,不要一封一封的下载

定位优化:

a.如果只是需要快速确定用户位置,最好用CLLocationManager的requestLocation方法,定位完成以后,会自动定位硬件断电

b.如果不是导航应用,尽量不要实时更新位置定位完毕就关闭定位服务

c.尽量降低定位精度比如尽量不要使用精度最高的kCLLocationAccuracyBest

d.需要后台定位时,尽量设置pausesLocationUpdatesAutomatically为YES,如果用户不太可能移动时候系统自动暂停位置更新

(三)App启动优化

1)App启动可以分为2种

启动(Cold Launch):从零开始启动App

热启动(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的常用函数

GCD有两个用来执行任务函数

a.用同步的方式执行任务

dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);

queue:队列

block:任务

   dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_sync(queue, ^{
        NSLog(@"执行任务~%@",[NSThread currentThread]);
    });

 同步是在当前线程执行任务

b.用异步的方式执行任务

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

所以只要是同步函数,只会在当前线程里面执行任务,就是按顺序来执行的

3)容易混淆术语

有4个比较容易混淆术语同步,异步,并发串行

同步和异步主要影响:能不能开启新的线程

同步:在当前线程中执行任务,不具备开启新线程的能力

异步:在新的线程中执行任务,具备开启新线程的能力

并发串行的主要影响是:任务的执行方式

并发多个任务并发(同时)进行

串行:一个任务执行完毕以后,再执行下一个任务

异步只是具备了开启新线程的能力,但是不一定开新的线程

 dispatch_queue_t mainQueue = dispatch_get_main_queue();
    //主队列
    dispatch_async(mainQueue, ^{
        NSLog(@"主线程执行任务3~%@",[NSThread currentThread]);
    });

如果是主队列的话,没有开启新的线程,是在主线程中执行

主队列是一个特殊的串行队列

4)各种队列的执行效果

​​​​​​​

5)面试题

a.以下代码会不会产生死锁(会)

- (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方法

一个是在47秒的时候,一个是在50秒的时候

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.支付宝支付

1)支付宝开发平台

https://open.alipay.com

2)移动支付集成

基本术语 | 网页&移动应用

3)商户服务平台(与支付宝签约需要填写公司资料):

https://b.alipay.com/page/portal/home

4)支付流程

a.在商户服务平台先与支付宝签约,获得商户ID(partner)和账号ID(seller),需要提供公司资质或者营业执照,个人无法申请

b.生成下载相应的公钥私钥文件(加密签名用)

c.下载支付宝SDK:

d.生成订单信息

e.调用支付宝客户端,由支付宝客户端支付宝安全服务器打交道

f.支付完毕后返回支付结果给商户客户端服务器

一、ios内存管理之野指针

1.野指针和空指针的概念

C语言中的野指针:声明一个指针变量,但是没有赋初始值,此时指针指向一个垃圾值,即指向一块随机内存空间

OC中的野指针:指针所指向的对象已经被释放/回收了,但是指针没有作任何的修改,仍然指向已经回收内存空间

空指针:没有指向任何东西的指针,即nil、NULL、0,向空指针发送消息不会报错

C中野指针事例: 此时没有对str2进行赋值,所以打印出来会是一个随机值,而且程序报错

OC中野指针事例:创建一个Person类的对象,创建完成之后将她release掉,此时的p就变成了野指针,需要注意的是虽然是野指针,但是不会报上述错误,如果用对象调用其方法,也是可以执行。(是因为此时对象中的数据可能在内存中)

2.僵尸对象(Zombie Object)

(1)僵尸对象:是一种用来检查内存错误(EXC_BAD_ACCESS)的对象,它可以捕获任何尝试访问坏内存的调用

(2)如果给僵尸对象发送消息,那么将在运行期间崩溃输出错误日志,可以通过日志定位到野指针对象调用的方法和类名

(3)通俗来讲:僵尸对象就是指一个引用计数器为0、被释放的OC对象,此时这个对象的内存已经被系统回收,该对象可能存在,即数据依然在内存中,但是此时僵尸对象已经不稳定了,其内存可能随时被别的对象申请占用,所以此时僵尸对象是不可以再访问和使用的。

3.内存回收的本质

(1)申请一块内存空间,其本质是向系统申请一块别人不再使用的内存空间,即已经回收内存空间

(2)释放一块内存空间,其本质是这个内存空间不再使用,可以由系统分配别的对象使用,此时内存空间虽然回收了,但是原本的数据依赖存在的,可以理解垃圾数据,所以内存回收可以理解为以下两点

@1 OC对象释放后,内存回收,表示这一块内存可以分配别的对象了;

@2 这块内存在分配别的对象之前,仍然保留着已经释放对象的数据

4.xcode能够检测僵尸对象,但是为什么检查

答:(1)会影响开发效率,(2)直接报错,来检查错误就可以

5.检测野指针,

在OC中野指针错误不是必现的,比如上面的例子我们可以开始僵尸对象检测机制

比如一行这样的代码

然后我们开启僵尸对象检测工具选择Edit Scheme进入到下面的选项框,然后勾选检测僵尸对象按钮

此时再运行项目

 

6. 将Person类变成了NSZombie类的底层实现分析

(1)打开Instruments工具,选择zombie对象检测

(2)进入检测页面,然后进行执行,将选择栏选到call tree下面,点击main函数进行分析,可以看到release方法其实是实现了父类release方法

(3)可以看到底层调用了__dealloc_zombie方法,我们可以点击进去,直接复制这个方法

二、谈一谈你对ios内存管理理解

答:内存管理的主要作用用来存储数据的,通过声明一个变量,可以将数据存储进去,

内存主要分为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里面的强引用

解决方案

解决方案一:使用block方式的初始

__weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf timeTest];
    }]

解决方案二:创建一个中间量target,让他弱引用viewController。

 创建一个类继承自NSObject

LJProxy.h文件

#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

LJProxy.m文件:

#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.如何创建一个延展

1)右击文件夹选择New File

2)选择Objective-C File

3)选择extension

2.延展的介绍 

1)延展是1个特殊分类,所以延展也是类的一部分

2)特殊之处:

a.延展这个特殊的分类没有名字

b.只有声明没有实现,和本类共享一个实现

3.延展的语法

@interface类名 ()

@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个特殊的分类,没有名字

2) 延展只有声明,没有单独的实现,和本类共享一个实现

5.延展和分类的区别

1)分类名字,而延展是一个匿名的分类,没有名字

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进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注