内存管理/run time机制

作者: 承宇 | 来源:发表于2015-11-04 20:05 被阅读678次
    Tables Are PS
    1 基本原理
    1.1 为什么要进行内存管理
    1.2 对象的基本结构
    1.3 引用计数器的作用
    1.4 操作
    1.5 对象的销毁
    2 相关概念和使用注意
    3 内存管理原则
    3.1 原则
    3.2 多对象管理
    4 内存管理代码规范
    4.1 基本数据类型
    4.2 内存管理代码规范
    4.2 dealloc方法的代码规范
    5 @property的参数
    5.1 内存管理相关参数
    5.2 set和get方法的名称
    6 内存管理中的循环引用问题以及解决
    7 Autorelease
    7.1 基本用法
    7.2 好处
    7.3 错误写法
    7.4 自动释放池
    7.5 使用注意
    8 ARC内存管理机制
    8.1 ARC的判断准则
    8.2 指针分类
    8.3 ARC的特点总结
    8.4 NSString的retainCount
    8.4 补充
    9 runtime 机制
    9.1 run time 相关原理
    9.2 runtime 的相关实现

    内存分配
    instruments 里面的leaks和allocation

    OC 内存管理

    1.基本原理

    1.1 为什么要进行内存管理

    由于移动设备的内存极其有限,所以每个APP所占的内存也是有限制的,当app所占用的内存较多时,系统就会发出内存警告,这时需要回收一些不需要再继续使用的内存空间,比如回收一些不再使用的对象和变量等。
    下列行为都会增加一个app的内存占用

    • 创建一个OC对象
    • 定义一个变量
    • 调用一个函数或者方法

    管理范围:任何继承NSObject的对象,对其他的基本数据类型无效。
    本质原因是因为对象和其他数据类型在系统中的存储空间不一样,其它局部变量主要存放于栈中,而对象存储于堆中,当代码块结束时这个代码块中涉及的所有局部变量会被回收,指向对象的指针也被回收.此时对象已经没有指针指向,要是依然存在于内存中,会造成内存泄露。

    1.2 对象的基本结构

    每个OC对象都有自己的引用计数器,是一个整数表示对象被引用的次数,即现在有多少东西在使用这个对象。对象刚被创建时,默认计数器值为1,当计数器的值变为0时,则对象销毁。
    在每个OC对象内部,都专门有4个字节的存储空间来存储引用计数器。

    1.3 引用计数器的作用

    判断对象要不要回收的唯一依据就是计数器是否为0,若不为0则存在。为0则调用dealloc函数。

    1.4 操作

    给对象发送消息,进行相应的计数器操作。
    Retain消息:使计数器+1,改方法返回对象本身
    Release消息:使计数器-1(并不代表释放对象)
    retainCount消息:获得对象当前的引用计数器值

    1.5 对象的销毁

    当一个对象的引用计数器为0时,那么它将被销毁,其占用的内存被系统回收。
    当对象被销毁时,系统会自动向对象发送一条dealloc消息,一般会重写dealloc方法,在这里释放相关的资源,dealloc就像是对象的“临终遗言”。一旦重写了dealloc方法就必须调用[super dealloc],并且放在代码块的最后调用(不能直接调用dealloc方法)。
    一旦对象被回收了,那么他所占据的存储空间就不再可用,坚持使用会导致程序崩溃(野指针错误)。

    2.相关概念和使用注意

    野指针错误:访问了一块坏的内存(已经被回收的,不可用的内存)。
    僵尸对象:所占内存已经被回收的对象,僵尸对象不能再被使用。(打开僵尸对象检测)



    空指针:没有指向任何东西的指针(存储的东西是0,null,nil),给空指针发送消息不会报错
    注意:不能使用[p retaion]让僵尸对象起死复生。

    3.内存管理原则

    3.1原则

    只要还有人在使用某个对象,那么这个对象就不会被回收;
    只要你想使用这个对象,那么就应该让这个对象的引用计数器+1;
    当你不想使用这个对象时,应该让对象的引用计数器-1;

    • 谁创建,谁release
      如果你通过alloc,new,copy来创建了一个对象,那么你就必须调用release或者autorelease方法
      不是你创建的就不用你去负责
    • 谁retain,谁release**
      只要你调用了retain,无论这个对象时如何生成的,你都要调用release
    • 总结
      有始有终,有加就应该有减。曾经让某个对象计数器加1,就应该让其在最后-1.

    3.2 多对象管理

    单个对象的内存管理, 看起来非常简单。如果对多个对象进行内存管理, 并且对象之间是有联系的, 那么管理就会变得比较复杂

    其实, 多个对象的管理思路 跟 很多游戏的房间管理差不多
    比如斗地主 \ QQ堂

    有一个房间,有人要用,一号使用retaincount+1 ,release下,二号使用同理。当没人使用的话,释放房间
    总的来说, 有这么几点管理规律

    只要还有人在用某个对象,那么这个对象就不会被回收
    只要你想用这个对象,就让对象的计数器+1
    当你不再使用这个对象时,就让对象的计数器-1

    4.内存管理代码规范

    只要调用了alloc,就必须有release(autorelease)

    4.1 基本数据类型:直接复制

    -(void)setAge:(int)age
    {
    _age=age;
    }
    

    4.2 OC对象类型

    -(void)setCar:(Car *)car
    {
    //1.先判断是不是新传进来的对象
    If(car!=_car)
    {
    //2 对旧对象做一次release
    [_car release];//若没有旧对象,则没有影响
    //3.对新对象做一次retain
    _car=[car retain];
    }
    }
    

    4.3 dealloc方法的代码规范

    • 一定要[super dealloc],而且要放到最后
    • 对self(当前)所拥有的的其他对象做一次release操作
    -(void)dealloc
    {
    [_car release];
    [super dealloc];
    }
    

    下面代码都会引发内存泄露

    p.dog = [[Dog alloc] init];
    
    [[Dog alloc] init].weight = 20.8;
    

    5. @property的参数

    5.1内存管理相关参数

    Retain:对对象release旧值,retain新值(适用于OC对象类型)完整的set方法,管理内存的参数
    Assign:直接赋值(默认,不写参数的都是assign,适用于非oc对象类型,这样对位于栈的系统自动生成的变量)
    Copy:release旧值,copy新值,用于NSString\block等类型

    ARC中的@property
    strong : 用于OC对象, 相当于MRC中的retain
    weak : 用于OC对象, 相当于MRC中的assign
    assign : 用于基本数据类型, 跟MRC中的assign一样
    copy : 一般用于NSString, 跟MRC中的copy一样

    是否要生成set方法(若为只读属性,则不生成)
    Readonly:只读,只会生成getter的声明和实现
    Readwrite:默认的,同时生成setter和getter的声明和实现

    多线程管理(苹果在一定程度上屏蔽了多线程操作)
    Nonatomic:高性能,一般使用这个
    Atomic:低性能

    5.2 Set和get方法的名称

    修改set和get方法的名称,主要用于布尔类型。因为返回布尔类型的方法名一般以is开头,修改名称一般用在布尔类型中的getter。
    @propery(setter=setAbc,getter=isRich) BOOL rich;
    BOOL b=p.isRich;// 调用

    6. 内存管理中的循环引用问题以及解决

    案例:每个人有一张身份证,每张身份证对应一个人,不能使用#import的方式相互包含,这就形成了循环引用。
    新的关键字:
    @class 类名;——解决循环引用问题,提高性能;
    @class仅仅告诉编译器,在进行编译的时候把后面的名字作为一个类来处理。
    @class的作用:声明一个类,告诉编译器某个名称是一个类

    那么我们怎么使用@class,

    1. 在.h文件中使用@class来声明类
    2. 在.m文件中真正要使用到的时候,使用#import来包含类中的所有东西

    两端循环引用的解决方法

    循环retain的场景
    比如A对象retain了B对象,B对象retain了A对象

    循环retain的弊端
    这样会导致A对象和B对象永远无法释放

    循环retain的解决方案
    当两端互相引用时,应该一端用retain、一端用assign(使用assign的在dealloc中也不用再release)

    7 Autorelease

    7.1 基本用法

    1. 会将对象放到一个自动释放池中
    2. 当自动释放池被销毁时,会对池子里的所有对象做一次release
    3. 会返回对象本身
    4. 调用完autorelease方法后,对象的计数器不受影响(销毁时影响)

    7.2 好处

    • 不需要再关心对象释放的时间
    • 不需要再关心什么时候调用release

    7.3 错误写法

    连续调用多次autorelease,释放池销毁时执行两次release(-1吗?)

    Alloc之后调用了autorelease,之后又调用了release。

    7.4 自动释放池

    在ios程序运行过程中,会创建无数个池子,这些池子都是以栈结构(先进后出)存在的。
    当一个对象调用autorelease时,会将这个对象放到位于栈顶的释放池中

    7.5 Autorelease注意

    1. 系统自带的方法中,如果不包含alloc new copy等,则这些方法返回的对象都是autorelease的,如[NSDate date];
    2. 开发中经常会写一些类方法来快速创建一个autorelease对象,创建对象时不要直接使用类名,而是使用self
    3. 占用内存较大的对象,不要随便使用autorelease,应该使用release来精确控制;而占用内存较小的对象使用autorelease,没有太大的影响

    8. ARC内存管理机制

    8.1 ARC的判断准则

    只要没有强指针指向对象,对象就会被释放。

    8.2 指针分类

    • 强指针:默认的情况下,所有的指针都是强指针,关键字strong
    • 弱指针:___weak关键字修饰的指针

    声明一个弱指针如下:___weak Person *p;
    ARC中,只要弱指针指向的对象不在了,就直接把弱指针做清空操作。

    ___weak Person * p= [[Person alloc] init];

    不合理,对象一创建出来就被释放掉,对象释放掉后,ARC把指针自动清零。
    ARC中在property处不再使用retain,而是使用strong,在dealloc中不需要再[super dealloc]。
    @property(nonatomic,strong)Dog *dog;// 意味着生成的成员变量_dog是一个强指针,相当于以前的retain。

    如果换成是弱指针,则换成weak,不需要加 ____ 。

    8.3 ARC的特点总结

    1. 不允许调用release,retain,retainCount
    2. 允许重写dealloc,但是不允许调用[super dealloc]
    3. @property的参数:
      Strong:相当于原来的retain(适用于OC对象类型),成员变量是强指针
      Weak:相当于原来的assign,(适用于oc对象类型),成员变量是弱指针
      Assign:适用于非OC对象类型(基础类型)

    8.4 NSString的retainCount

    ** 提示:字符串是特殊的对象,但不需要使用release手动释放,这种字符串对象默认就是autorelease的,不用额外的去管内存,存放到堆中。 **

    //1. 字符串常量
    NSString *str1 = @"string";
    NSLog(@"str1: %ld",[str1 retainCount]); // -1或18446744073709551615(即UINT_MAX ( Maximum value an `unsigned int'))
    
    //因为"str"为字符串常量,系统不会收回,也不会对其作引用计数,即使我们对str如何retain或release。就是为-1.
     
    //2. stringWithFormat
    NSString *str2 = [NSString stringWithFormat:@"%s", "test"];
    NSLog(@"str2:%ld",[str2 retainCount]); // 1
    //使用stringWithFormat创建的NSString为变量,系统会进行引用计数。  以前为1,现在为-1.
     
    //3. stringWithString
    stringWithString这个方法比较特别:它的retainCount取决于它后面跟的string对象
    
    NSString *str3 = [NSString stringWithString:@"test"];
    NSString *str4 = [NSString stringWithString:[NSString stringWithFormat:@"test,%d  ,%d",1,3]];
    NSLog(@"str3:%ld",[str3 retainCount]); // -1
    NSLog(@"str4:%ld",[str4 retainCount]); // 2
    //可以看到第一个为"常量"对象,其retainCount方法的实现返回的是maxIntValue。
    //第二个为2,这个方法生成的只是一个对另一个对象的引用。一个对象实例,两次的stringWithString,它的retainCount为2,同时都被当前的AutoreleasePool管理。
    

    8.5 补充

    ** 让程序兼容ARC和非ARC部分。转变为非ARC -fno-objc-arc 转变为ARC的, -f-objc-arc 。**
    ARC也需要考虑循环引用问题:一端使用retain,另一端使用assign。


    set  
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Person *p = [Person new];
            Car *car = [Car new];
            car.speed = 100;
            [p setCar:car];
            [p drive];
            
            [car release];
            
            /*
            //重复调用,已经释放了,要改加上if语句去进行判断
            [p setCar:car];
            [p drive];
            */
            
            Car *car2 = [Car new];
            car2.speed = 200;
            [p setCar:car2];
            
            
            [p drive];
            [car2 release];
            [p  release];
    
    
        }
        return 0;
    }  
    
    #import <Foundation/Foundation.h>
    #import "Car.h"
    @interface Person : NSObject
    {
        Car *_car;
        NSString *_name;
    }
    
    - (void)setName:(NSString *)name;
    - (NSString *)name;
    - (void)setCar:(Car *)car;
    - (Car *)car;
    - (void)drive;
    @end  
    
    #import "Person.h"
    
    @implementation Person
    - (void)dealloc{
        //目的是要保证在p对象存在的时候,car对象一定存在
        //对象p被销毁的时候
        [_car release];
        [_name release];
        [super dealloc];
        NSLog(@"被释放了");
    }
    
    - (void)setName:(NSString *)name{
        if (_name != name) {
            [_name release];
            _name = [name retain];
        }
    
    }
    - (NSString *)name
    {
        return _name;
    }
    
    
    - (void)setCar:(Car *)car{
        //第一次是空,所以release无所谓,第二次减一,要是不release的话,会造成内存泄露的问题。
        //car retainCount= 1
        if (_car != car) {
        [car release];
        }
       
        //car2 2
        _car = [car retain];//引用计数加1 ,返回self
    }
    - (void)drive{
        [_car run];
    }
    @end  
    
    #import <Foundation/Foundation.h>
    
    @interface Car : NSObject
    @property int speed;
    
    - (void)run;
    @end  
    
    #import "Car.h"
    
    @implementation Car
    - (void)dealloc{
        [super dealloc];
        NSLog(@"被释放了  %d",_speed);
    }
    - (void)run{
        NSLog(@"I can run");
    }
    @end
    
    

    9. runtime 机制

    9.1 run time 相关原理

    对于runtime机制,在网上找到的资料大概就是怎么去用这些东西,以及查看runtime.h头文件中的实现,当然这确实是一种很好的学习方法,但是,其实我们还是不会知道runtime底层编译成C++语言之后做了什么? 查到一个大牛给资料,顿时对runtime有了一定认识!

    我们随便写一个小程序,代码如下: person类头文件如下,

    <!-- lang: cpp -->
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    @property (nonatomic, strong) NSString *name; 
    @property (nonatomic, assign) int age;
    
    @end
    
    main.m文件如下
    <!-- lang: cpp -->
    int main(int argc, const char * argv[])
    
    {
    Person *p = [[Person alloc] init];
    NSString *str = @"zhangsan";
    p.name = str;
    // p.name 等价于
    [p setName:str];
    p.age = 20;
    return 0;
    
    }
    

    然后我们打开终端,在命令行找到cd到文件目录,然后中输入:
    clang -rewrite-objc main.m

    命令可以将main.m编译成C++的代码,改成不同的文件名,就会生成不同的c++代码 这是就生成了main.cpp这个c++文件,打开文件代码 查看该main.cpp最底下的main函数, 这样我们就可以看到底层具体实现的方式!
    这时,我们就需要知道这些方法:
    **
    objc_msgSend 可以给对象发送消息
    objc_getClass(“Person”) 可以获取到指定名称的对象
    sel_registerName(“alloc”) 可以调用到对象的方法
    **

    通过查看,c++代码,我们得出结论:
    使用objc_msgSend函数,给objc_getClass函数实例化的对象发送sel_registerName获取到的方法 这么一个消息 代码是给人看的,顺带让机器实现功能。
    日常的程序开发过程中,要少用runtime,
    那什么时候会使用runtime呢? runtime应用的时机:
    1> 当需要非常高的性能开发时,使用runtime,注释:oc的代码已经无法满足性能需求
    2> 当我们对系统内部的实现很好奇的时候,可以用clang反编译成c++去看底层的实现机制!

    项目讲解的是runtime的底层实现原理, 如果想要知道runtime是怎么用的,可以查看runtime.h头文件查看! 以下是runtime机制方法的一些使用方法介绍,希望对大家有用!
    相关技术文档:
    http://www.tuicool.com/articles/uimInm http://blog.csdn.net/lengshengren/article/details/17764135

    9.2runtime 的相关实现

    首先,第一个问题,

    1. runtime实现的机制是什么?

    runtime是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API。 在我们平时编写的OC代码中, 程序运行过程时, 其实最终都是转成了runtime的C语言代码, runtime算是OC的幕后工作者比如说,下面一个创建对象的方法中, 举例: OC :

     [[MJPerson alloc] init] 
    runtime : 
    objc_msgSend(objc_msgSend(“MJPerson” , “alloc”), “init”)
    
    1. runtime 用来干什么呢??用在那些地方呢?怎么用呢?

    runtime是属于OC的底层, 可以进行一些非常底层的操作(用OC是无法现实的, 不好实现)

    • 在程序运行过程中, 动态创建一个类(比如KVO的底层实现)
    • 在程序运行过程中, 动态地为某个类添加属性\方法, 修改属性值\方法
    • 遍历一个类的所有成员变量(属性)\所有方法

    例如:我们需要对一个类的属性进行归档解档的时候属性特别的多,这时候,我们就会写很多对应的代码,但是如果使用了runtime就可以动态设置!

    例如,PYPerson.h的文件如下所示

    import
    
    @interface PYPerson : NSObject 
    @property (nonatomic, assign) int age; 
    @property (nonatomic, assign) int height;
    @property (nonatomic, copy) NSString *name; 
    @property (nonatomic, assign) int age2; 
    @property (nonatomic, assign) int height2; 
    @property (nonatomic, assign) int age3; 
    @property (nonatomic, assign) int height3; 
    @property (nonatomic, assign) int age4; 
    @property (nonatomic, assign) int height4;
    @end
    

    而PYPerson.m实现文件的内容如下

    
    #import "PYPerson.h"
    
    import
    @implementation  PYPerson
    - (void)encodeWithCoder:(NSCoder *)encoder {
     unsigned int count = 0; 
    Ivar *ivars = class_copyIvarList([PYPerson class], &count);
    
    for (int i = 0; i<count; i++) {
    
    // 取出i位置对应的成员变量Ivar ivar = ivars[i];
    // 查看成员变量const char *name = ivar_getName(ivar);
    // 归档NSString *key = [NSString stringWithUTF8String:name];
    id value = [self valueForKey:key];
    [encoder encodeObject:value forKey:key];
    }
    free(ivars); 
    }
    
    (id)initWithCoder:(NSCoder *)decoder { 
    if (self = [super init]) {
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([PYPerson class], &count);for (int i = 0; i<count; i++) { 
    // 取出i位置对应的成员变量 Ivar ivar = ivars[i]; 
    // 查看成员变量 const char *name = ivar_getName(ivar);
     // 归档 NSString *key = [NSString stringWithUTF8String:name]; id value = [decoder decodeObjectForKey:key]; 
    // 设置到成员变量身上 [self setValue:value forKey:key];}free(ivars);
    
    }
     return self; 
    }
    
    @end
    

    这样我们可以看到归档和解档的案例其实是runtime写下的.

    学习runtime机制首先要了解下面几个问题

    1. 相关的头文件和函数
      利用头文件,我们可以查看到runtime中的各个方法!

    2. 相关应用
      NSCoding(归档和解档, 利用runtime遍历模型对象的所有属性)
      字典 –> 模型 (利用runtime遍历模型对象的所有属性, 根据属性名从字典中取出对应的值, 设置到模型的属性上)
      KVO(利用runtime动态产生一个类)
      用于封装框架(想怎么改就怎么改) 这就是我们runtime机制的只要运用方向

    3. 相关函数
      objc_msgSend : 给对象发送消息
      class_copyMethodList : 遍历某个类所有的方法
      class_copyIvarList : 遍历某个类所有的成员变量
      class_….. 这是我们学习runtime必须知道的函数!

    4. 必备常识 1> Ivar : 成员变量 2> Method : 成员方法 从上面例子中我们看到我们定义的成员变量,如果要是动态创建方法,可以使用Method.

    相关文章

      网友评论

      • 0eaf89c16462:临睡觉发现的好文,一下看到3点,🙏

      本文标题:内存管理/run time机制

      本文链接:https://www.haomeiwen.com/subject/tsvicttx.html