APP内存管理

简介: APP内存管理

1、iOS程序的内存布局


8752c71ea4ca45e3a28f8c270ef4b6f3.png

1、代码段:编译之后的代码

2、数据段


2.1、字符串常量:比如NSString *str = @“123”

2.2、已初始化数据:已初始化的全局变量、静态变量等

2.3、未初始化数据:未初始化的全局变量、静态变量等


3、堆:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大

4、栈:函数调用开销,比如局部变量。分配的内存空间地址越来越小


2、Tagged Pointer 标记指针

1、从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储.

2、在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值。使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中。(Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free。)

3、当指针不够存储数据时,才会使用动态分配内存的方式来存储数据。

4、objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销。

5、在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。


优点:

1、减少了 64 位机器下程序的内存占用,还提高了运行效率。完美地解决了小内存对象在存储和访问效率上的问题。


弊病:

1、因为并不是真正的对象,而是一个伪对象,是没有isa指针的。

2、因为不是真正的对象,所以如果你直接访问Tagged Pointer的isa成员的话,在编译时将会得到警告。这时我们只要避免在代码中直接访问对象的 isa 变量,即可避免这个问题。


判断是否为Tagged Pointer:


1、iOS平台,最高有效位是1(第64bit)

2、Mac平台,最低有效位是1


#if TARGET_OS_OSX & __x86_64__
    //64-bit Mac tag bit is LSB
#   define OBJC_MSB_TAGGED_POINTERS 0
#else
    //Everything else tag bit is MSB
#   define OBJC_MSB_TAGGED_POINTERS 1
#endif
#if OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#else
#   define _OBJC_TAG_MASK 1UL
#endif
static inline bool
_objc_isTaggedPointer(const void *_Nullable ptr) {
  return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}


问答拓展


思考以下2段代码能发生什么事?有什么区别?

98ba1b06c75c4880afaa5e90d42d91fe.png

1、第一段代码会导致闪退,因为第一段代码每次赋值都是给地址赋值,每次setter之前都会执行release操作。因为是异步操作,所以可能会导致多次release,导致引用计数已经小于等于0,对象已经没销毁。最终抛出坏内存异常。

2、第二段代码属于指针赋值,直接赋值,没有引用计数操作,所以没有问题。


3、定时器


已知iOS中常用的定时器有三种:NSTimer、CADisplayLink、GCD。

它们有各自的特性和应用场景:

1、NSTimer


1.1、存在延迟:不管是一次性的还是周期性的timer的实际触发事件的时间,都会与所加入的RunLoop和RunLoop Mode有关,如果此RunLoop正在执行一个连续性的运算,timer就会被延时出发。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行。

1.2、必须加入Runloop:注意与uiscrollview的使用,要切换RunLoopMode状态。


2、CADisplayLink


1、保证调用频率和屏幕的刷帧频率一致,60FPS。

2、如果调用的方法比较耗时,超过了屏幕刷新周期,就会导致跳过若干次回调调用机会,跳过次数取决CPU的忙碌程度。

3、CADisplayLink适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染。


3、GCD


1、时间准确

2、可以使用子线程,解决定时间跑在主线程上卡UI问题。


3.1、NSTimer、CADisplayLink的使用注意点

查看以下代码能否正常编译运行?会出现什么问题?需要怎么实现?


@interface ViewController ()
@property (strong, nonatomic) CADisplayLink *link;
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
 - (void)timerTest {
    NSLog(@"%s", __func__);
}
- (void)linkTest {
    NSLog(@"%s", __func__);
}
- (void)dealloc {
    NSLog(@"%s", __func__);
    [self.link invalidate];
    [self.timer invalidate];
}
 @end   


以上代码可以正常编译运行,但是离开这个页面时,因为它们之间互相强引用,导致内存无法正常释放(内存泄漏)。以timer为例,见下图:

76718b3751404afa8e75f7e8988b60ce.png


解决方案,可以在两者之间添加一个弱引用,见下图:

d3b7b1d7702e46adb6170d98f89b1ce7.png

这样当ViewController销毁时,发现没有其他对象强引用它,那么整个相关链条就可以正常销毁。


代码实现:


方法一、使用block

timer可以使用block弱引用实现


- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf timerTest];
    }];
}
- (void)timerTest {
    NSLog(@"%s", __func__);
}


方法二、使用代理对象(NSProxy)

1、NSProxy是专为代理而生,当调用没有实现的方法时,直接触发消息转发。

2、如果继承的事NSObject时,会先触发消息发送机制,如果没有时才会进入消息转发阶段,更耗时耗性能。


ZMProxy.h:


#import <Foundation/Foundation.h>
@interface ZMProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

ZMProxy.m:


#import "ZMProxy.h"
@implementation ZMProxy
+ (instancetype)proxyWithTarget:(id)target {
    ZMProxy *proxy = [[ZMProxy alloc] init];
    proxy.target = target;
    return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}
@end


ZMProxy1.h:


#import <Foundation/Foundation.h>
@interface ZMProxy1 : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end


ZMProxy1.m:


#import "ZMProxy1.h"
@implementation ZMProxy1
+ (instancetype)proxyWithTarget:(id)target {
    ZMProxy1 *proxy = [[ZMProxy1 alloc] init];
    proxy.target = target;
    return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.target;
}
@end


- (void)viewDidLoad {
    [super viewDidLoad];
    self.link = [CADisplayLink displayLinkWithTarget:[ZMProxy proxyWithTarget:self] selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[ZMProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
    NSLog(@"%s", __func__);
}
- (void)linkTest {
    NSLog(@"%s", __func__);
}
- (void)dealloc {
    NSLog(@"%s", __func__);
    [self.link invalidate];
    [self.timer invalidate];
}


3.2、GCD定时器封装

ZMTimer.h:


#import <Foundation/Foundation.h>
@interface ZMTimer : NSObject
/// 开启定时器【代码块回调】
/// @param task 任务
/// @param start 开始时间
/// @param interval 时间间隔
/// @param repeats 是否重复
/// @param async 是否异步
/// @return 返回定时器标记
+ (NSString *)execTask:(void(^)(void))task
                 start:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
               repeats:(BOOL)repeats
                 async:(BOOL)async;
/// 开启定时器【SEL实现】
/// @param target 添加对象
/// @param selector 方法实现
/// @param start 开始时间
/// @param interval 时间间隔
/// @param repeats 是否重复
/// @param async 是否异步
/// @return 返回定时器标记
+ (NSString *)execTask:(id)target
              selector:(SEL)selector
                 start:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
               repeats:(BOOL)repeats
                 async:(BOOL)async;
/// 取消定时器任务
/// @param name 定时器标记
+ (void)cancelTask:(NSString *)name;
@end


ZMTimer.m:


#import "ZMTimer.h"
@implementation ZMTimer
static NSMutableDictionary *timers_;
dispatch_semaphore_t semaphore_;
+ (void)initialize {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        timers_ = [NSMutableDictionary dictionary];
        semaphore_ = dispatch_semaphore_create(1);
    });
}
/// 开启定时器【代码块回调】
/// @param task 任务
/// @param start 开始时间
/// @param interval 时间间隔
/// @param repeats 是否重复
/// @param async 是否异步
/// @return 返回定时器标记
+ (NSString *)execTask:(void (^)(void))task
                 start:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
               repeats:(BOOL)repeats
                 async:(BOOL)async {
    if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
    // 1、创建队列
    dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
    // 2、创建定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    // 3、设置时间
    dispatch_source_set_timer(timer,
                              dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
                              interval * NSEC_PER_SEC, 0);
    // 加锁
    dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
    // 定时器的唯一标识
    NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
    // 4、存放到字典中
    timers_[name] = timer;
    // 解锁
    dispatch_semaphore_signal(semaphore_);
    // 5、设置回调
    dispatch_source_set_event_handler(timer, ^{
        task();
        if (!repeats) { // 不重复的任务
            [self cancelTask:name];
        }
    });
    // 6、启动定时器
    dispatch_resume(timer);
    return name;
}
/// 开启定时器【SEL实现】
/// @param target 添加对象
/// @param selector 方法实现
/// @param start 开始时间
/// @param interval 时间间隔
/// @param repeats 是否重复
/// @param async 是否异步
/// @return 返回定时器标记
+ (NSString *)execTask:(id)target
              selector:(SEL)selector
                 start:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
               repeats:(BOOL)repeats
                 async:(BOOL)async {
    if (!target || !selector) return nil;
    return [self execTask:^{
        if ([target respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [target performSelector:selector];
#pragma clang diagnostic pop
        }
    } start:start interval:interval repeats:repeats async:async];
}
/// 取消定时器任务
/// @param name 定时器标记
+ (void)cancelTask:(NSString *)name {
    if (name.length == 0) return;
    dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
    dispatch_source_t timer = timers_[name];
    if (timer) {
      // 7、取消定时器
        dispatch_source_cancel(timer);
        [timers_ removeObjectForKey:name];
    }
    dispatch_semaphore_signal(semaphore_);
}
@end


实现:


#import "ViewController.h"
#import "ZMTimer.h"
@interface ViewController ()
@property (copy, nonatomic) NSString *task;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.task = [ZMTimer execTask:self
                         selector:@selector(doTask)
                            start:2.0
                         interval:1.0
                          repeats:YES
                            async:NO];
//    self.task = [MJTimer execTask:^{
//        NSLog(@"111111");
//    } start:2.0 interval:-10 repeats:NO async:NO];
}
- (void)doTask {
    NSLog(@"222222");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [ZMTimer cancelTask:self.task];
}
@end


4、OC对象的内存管理


1、在iOS中,使用引用计数来管理OC对象的内存。

2、一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间。

2、调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1

3、内存管理的经验总结:


3.1、当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它。

3.2、想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1。


4、可以通过以下私有函数来查看自动释放池的情况

extern void _objc_autoreleasePoolPrint(void);


4.1、引用计数的存储

1、在64bit中,引用计数可以直接存储在优化过的isa指针中,也可能存储在SideTable类中。

4d0e06fe7e934335a3d8fe9e167507ad.png

2、refcnts是一个存放着对象引用计数的散列表。


4.2、dealloc

当一个对象要释放时,会自动调用dealloc,接下的调用轨迹是:


dealloc

_objc_rootDealloc

rootDealloc

object_dispose

objc_destructInstance、free

ca80c7cc4f9043b9a29a12b2eba37802.png



4.3、自动释放池

4.3.1、autorelease 底层结构

1、自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage。

2、调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的。

3、源码分析:


clang重写@autoreleasepool

objc4源码:NSObject.mm

d7a100e6aa6d43888781b778372eb1a0.png



4.3.2、AutoreleasePoolPage

1、链表关系

1、每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址

2、所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起。


da0693a1a4da455e951dbddc00c15e29.png


2、实现原理

1、调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址。

2、调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY。

3、id *next指向了下一个能存放autorelease对象地址的区域。


3、触发逻辑

iOS在主线程的Runloop中注册了2个Observer


1、第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()。

2、第2个Observer:


2.1、监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()。

2.2、监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()。


5、问答拓展


1、使用CADisplayLink、NSTimer有什么注意点?

2、介绍下内存的几大区域

3、讲一下你对 iOS 内存管理的理解

4、ARC 都帮我们做了什么?


LLVM(编译器) + Runtime(运行时)

编译器:如自动释放池的实现逻辑

运行时:比如弱引用的实现逻辑

总的来说:ARC是LLVM编译器和Runtime系统相互协作的结果。


5、weak指针的实现原理


当一个对象要释放时,会自动调用dealloc。底层实现:


obj->clearDeallocating();//将指向当前对象的指针置为nil

1

6、autorelease对象在什么时机会被调用release


什么时候调用release是由RunLoop来控制的,它可能是在某次RunLoop循环中,RunLoop休眠之前调用了release。


7、方法里有局部对象, 出了方法后会立即释放吗?


1、如果这个对象是autorelease对象,什么时候调用release是由RunLoop来控制的,它可能是在某次RunLoop循环中,RunLoop休眠之前调用了release。

2、如果不是autorelease对象,在方法结束之前,底层会调用release方法,释放这个局部对象。


相关文章
|
20天前
|
C# Windows
【Azure App Service】在App Service for Windows上验证能占用的内存最大值
根据以上测验,当使用App Service内存没有达到预期的值,且应用异常日志出现OutOfMemory时,就需要检查Platform的设置是否位64bit。
40 11
|
19天前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
|
4月前
|
Java 容器
【Azure Function App】Java Function在运行中遇见内存不足的错误
【Azure Function App】Java Function在运行中遇见内存不足的错误
|
7月前
|
监控 测试技术 Shell
APP的CPU,内存和流量如何测试?
APP的CPU,内存和流量如何测试?
113 0
|
7月前
|
测试技术 Android开发 UED
如何对APP的内存使用情况进行监测?
如何对APP的内存使用情况进行监测?
|
iOS开发
[✔️]xcode Instrucments排查app的内存泄露
[✔️]xcode Instrucments排查app的内存泄露
572 0
|
iOS开发 异构计算
如何增加 iOS APP 虚拟地址空间及内存上限?XNU 内核源码解读
1. 引言 最近一段时间在做钉钉 iOS 内存专项治理,解决内存不足时的 jetsam 事件及 malloc 的异常崩溃。在进程创建时系统会为每个 app 设定内存最大使用上限,内核会维护一个内存阈值优先级列表,当设备内存不足时低优先级的 app 会首先被内核中止进程。在阅读 XNU 内核源码过程中我们发现提供系统了两种能力可以扩展 App 的虚拟地址空间(com.apple.developer.kernel.extended-virtual-addressing)和增加内存使用上限(com.apple.developer.kernel.increased-memory-limit)。
2124 0
如何增加 iOS APP 虚拟地址空间及内存上限?XNU 内核源码解读
|
Android开发
Android进程守护,让APP在系统内存中常驻(二)
Android进程守护,让APP在系统内存中常驻
757 0
|
Android开发
Android进程守护,让APP在系统内存中常驻(一)
Android进程守护,让APP在系统内存中常驻
945 0
|
数据库 Android开发
Android | App内存优化 之 全面理解MAT
Android | App内存优化 之 全面理解MAT

热门文章

最新文章