代理详解

简介: 本文纯属个人观点,如有错处,敬请指正,不胜感激。我们不管是在项目中,还是在面试过程中,总是免不了被问及循环引用的话题,这还是要归于ios的内存管理机制——引用计数。

本文纯属个人观点,如有错处,敬请指正,不胜感激。

我们不管是在项目中,还是在面试过程中,总是免不了被问及循环引用的话题,这还是要归于ios的内存管理机制——引用计数。而循环引用常常发生的情景其中之一就是代理和block。这里重点说说代理。

一、代理解决什么问题

场景:
有一个婴儿,他要吃饭才能维持生存,但是,他自己并不能照顾自己。有一个育儿师,她有照顾婴儿的能力,但是她缺少收入。这个时候,婴儿和育儿师之间可以达成协议,婴儿委托给育儿师照顾,婴儿付出报酬。

提取关键词: 婴儿(委托方)、育儿师(代理方)、协议

我们可以根据这个场景,设计程序。

先来定义协议:BabyDelegate.h

协议是声明了一组方法的文件。协议只提供方法的名字,不提供实现。这个跟生活中的协议也是很形象的。协议就是一个文件,这个文件既不会干活儿,也不会发报酬,它就是一个约定。方法相等于协议中的条款。有些方法是必须实现的,有些是可以实现可以不实现的。这都要视具体情况而定。

#import <Foundation/Foundation.h>

@protocol BabyDelegate <NSObject>

-(void)takeCareOfBaby;

@end

\

Baby类

Baby.h:


#import <Foundation/Foundation.h>
#import "BabyDelegate.h"

@interface Baby : NSObject
//遵循协议的成员变量
 @property(nonatomic,weak)id<BabyDelegate> delegate; 

-(void)eat;

@end

Baby.m

#import "Baby.h"

@implementation Baby

-(void)eat{
//因为该成员变量遵循了协议,所以这里可以调用协议中声明的方法。
//至于方法有没有实现,编译阶段是不关心的,只有运行阶段才会知道。
//一旦方法没有实现,会造成崩溃。所以这里需要加上判断最好。
   if ([_delegate respondsToSelector:@selector(takeCareOfBaby)]) {
        [_delegate takeCareOfBaby];
    }
}

@end

Baby类已经构建完成了。下面是BabySitter类。

BabySitter.h

#import <Foundation/Foundation.h>
#import "BabyDelegate.h"

//注意这里:要想实现协议中的方法,一定要遵守协议
@interface  BabySitter : NSObject<BabyDelegate>

@end

BabySitter.m

#import "BabySitter.h"

@implementation BabySitter

//实现协议中声明的方法
-(void)takeCareOfBaby{
    NSLog(@"照顾小宝宝");
}

@end

注意观察,Baby、BabySitter以及协议BabyDelegate之间的关系。

到目前为止,Baby、BabySitter分别跟BabyDelegate建立了联系,但是Baby和BabySitter还没有任何的联系。协议的三方都有了,最后就是协议的触发了。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //创建一个育儿师对象
    BabySitter * xiaohong = [[BabySitter alloc] init];
   //创建一个婴儿对象
    Baby * xiaoming = [[Baby alloc] init];
  //重点来了,把婴儿和育儿师建立联系
    xiaoming.delegate = xiaohong;
  //此时,婴儿可以吃饭了,育儿师也可以拿到报酬了
    [xiaoming eat];
}

在执行 [xiaoming eat]; 这句代码的时候,执行过程是这样的:

1.先到Baby类中查看是否有eat方法,发现有,进入eat方法内部执行。
2.在eat方法体内部,是一个代理。这个代理的类是xiaohong,也就是babySitter类,那么就去判断该代理中的takeCareOfBaby方法是否在babySitter中进行了实现。如果实现了,[_delegate respondsToSelector:@selector(takeCareOfBaby)]返回true,执行[_delegate takeCareOfBaby]。
3.现在[_delegate takeCareOfBaby]等同于[xiaohong takeCareOfBaby],因为此时delegate已经指向xiaohong了。
4.在BabySitter类中,找到takeCareOfBaby方法,执行方法体内的语句。结果就会打印出“照顾小宝宝”。

现在我们基本可以回答以下几个问题了。

  1. 为什么Baby类中
    @property(nonatomic,weak)id<BabyDelegate> delegate;
    需要使用id泛型进行声明呢?

因为直到 xiaoming.delegate = xiaohong;这句话执行之前,delegate的类型都是不确定的。delegate可以指向任意类型。另一方便,无论实现代理的对象是什么类型,都必须要先遵循协议。

  1. 代理使用strong一定会循环引用?

答案是否定的。
为什么呢?
首先要明白什么是循环引用。
要说循环引用就不得不说ios的内存管理方法或者叫垃圾回收机制——引用计数。
简要说,当一个对象被创建出来之后(不管是怎么创建出来的),至少有一个强指针指向它,它的引用计数都是一个大于0的数。当有别的强引用指针指向这个对象的时候,该对象的引用计数就会增加1。当一个强引用指针从这个对象上移除的时候,这个对象的引用计数就会减1 。直到所有的指针都移除的时候,这个对象的引用计数就等于0了。这个时候,系统就会把这个对象回收掉了。至于系统是什么时候知道这个对象的引用计数等于0的,什么时候回收的,这又是另外的问题了。这里不做深入探讨了。

这里再插入一个小概念:指针和对象。

指针和对象密切相关,但是又是完全不同的东西。

创建个简单的Person类:
//
//  Person.h
//  TestOC
//
//  Created by iOS on 2018/4/20.
//  Copyright © 2018年 weiman. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property(nonatomic,copy) NSString * name;
@property(nonatomic,assign) NSInteger age;

- (void)play;

- (void)work;

@end

//
//  Person.m
//  TestOC
//
//  Created by iOS on 2018/4/20.
//  Copyright © 2018年 weiman. All rights reserved.
//

#import "Person.h"

@implementation Person

-(void)play {
    NSLog(@"周末一起去玩耍");
}

-(void)work {
    NSLog(@"工作中。。。");
}

@end

main中:

//
//  main.m
//  TestOC
//
//  Created by iOS on 2018/4/20.
//  Copyright © 2018年 weiman. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
        
        Person * tom = [[Person alloc]init];
        tom.name = @"Tom";
        
        Person * happy = tom;
        NSLog(@"%@, %@",tom, happy);
    }
    return 0;
}

看这句:
Person * tom = [[Person alloc]init];

从运算符来看,这是一句赋值语句。赋值语句的执行顺序是从右到左。也就是说,先执行 [[Person alloc]init]。
再来分析这句话:
[[Person alloc]init]其实执行了两个方法,一个是类方法alloc,定义在父类NSObject中。另一个是对象方法init,也是定义在父类NSObject中。
[Person alloc]:开辟一段内存空间。
init:对[Person alloc]出来的对象进行初始化。例如给对象age赋初始值0.
(系统如何给person对象开辟的空间,开辟多大空间,空间里都有什么,是如何排列的,初始化的时候都做了什么,成员变量和方法是如何被初始化的,这些问题还有待研究。)

[[Person alloc]init]这句话执行完成之后,我们现在有一个对象了,这个对象是Person类型的,没有指针指向它。

现在再来看赋值语句左边的语句:Person * tom
这一句话是声明了一个Person类型的 指针,指针的名字叫做tom。

最后执行赋值,把右边创建出来的对象地址赋值给指针tom,也就是把指针tom指向了这个创建出来的Person对象。可以使用这个指针访问这个对象的可见的属性和方法。

Person * happy = tom;
理解上上面的概念,这句话就好理解了,把tom指针赋值给happy指针,就是happy指针也指向了tom指向的对象的地址,此时,该对象已经有两个指针引用了。

由于tom和happy这两个指针变量都是局部变量,在方法体执行完之后,这两个指针都会被销毁,当两个指针都销毁的时候,这个Person的对象的引用计数就变成0了,现在系统就把这块空间回收了,这个对象也就销毁了。

哎呀呀呀······扯的有点远了。。。

说了那么多,到底和循环引用有什么关系呢?对象释放的前提是对象本身没有强引用。循环引用会发生就是因为,两个对象中分别持有对方的强引用指针。导致这两个对象都不能释放,这两块内存都不能回收,造成资源浪费,也称为内存泄漏。

内存泄漏为什么总是跟代理相关?

因为代理中持有别的对象的指针,也就是这句:

@property(nonatomic,weak)id<BabyDelegate> delegate;

如果delegate是一个强引用,而实现代理的BabySitter中恰好也有Baby的强引用指针。这个时候,Baby和BabySitter的对象就会造成循环引用。注意,关键来了,循环引用是一个双向的强引用,仅仅把代理声明成strong不一定就会造成循环引用,还要满足另一个条件:代理者中也有被代理者的强引用,这样才会循环引用。
所以,代理声明称strong的不一定会循环引用。

但是,为了安全起见(我们也不知道代理是谁,代理者中会不会有被代理者的强引用指针存在),还是把代理声明成weak的。

  1. 代理为什么不用assign声明?

weak和assign都是一种“非拥有关系”的指针。通过这两种关键字修饰的变量,都不会改变被引用对象的引用计数。但是,不同的是,在一个对象被释放之后,weak修饰的指针变量会被置为nil,而assign不会。在OC中,向nil发送消息不会崩溃。而使用assign,在对象被释放后,在使用这个指针调用被释放对象,也就是向这个对象发送消息,就会有崩溃出现。

这篇短短的文章断断续续写了好几次,终于写完了。

目录
相关文章
|
6天前
|
存储 关系型数据库 分布式数据库
PostgreSQL 18 发布,快来 PolarDB 尝鲜!
PostgreSQL 18 发布,PolarDB for PostgreSQL 全面兼容。新版本支持异步I/O、UUIDv7、虚拟生成列、逻辑复制增强及OAuth认证,显著提升性能与安全。PolarDB-PG 18 支持存算分离架构,融合海量弹性存储与极致计算性能,搭配丰富插件生态,为企业提供高效、稳定、灵活的云数据库解决方案,助力企业数字化转型如虎添翼!
|
17天前
|
弹性计算 关系型数据库 微服务
基于 Docker 与 Kubernetes(K3s)的微服务:阿里云生产环境扩容实践
在微服务架构中,如何实现“稳定扩容”与“成本可控”是企业面临的核心挑战。本文结合 Python FastAPI 微服务实战,详解如何基于阿里云基础设施,利用 Docker 封装服务、K3s 实现容器编排,构建生产级微服务架构。内容涵盖容器构建、集群部署、自动扩缩容、可观测性等关键环节,适配阿里云资源特性与服务生态,助力企业打造低成本、高可靠、易扩展的微服务解决方案。
1320 7
|
5天前
|
存储 人工智能 Java
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话
本文讲解 Prompt 基本概念与 10 个优化技巧,结合学术分析 AI 应用的需求分析、设计方案,介绍 Spring AI 中 ChatClient 及 Advisors 的使用。
297 129
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话
|
4天前
|
监控 JavaScript Java
基于大模型技术的反欺诈知识问答系统
随着互联网与金融科技发展,网络欺诈频发,构建高效反欺诈平台成为迫切需求。本文基于Java、Vue.js、Spring Boot与MySQL技术,设计实现集欺诈识别、宣传教育、用户互动于一体的反欺诈系统,提升公众防范意识,助力企业合规与用户权益保护。
|
16天前
|
机器学习/深度学习 人工智能 前端开发
通义DeepResearch全面开源!同步分享可落地的高阶Agent构建方法论
通义研究团队开源发布通义 DeepResearch —— 首个在性能上可与 OpenAI DeepResearch 相媲美、并在多项权威基准测试中取得领先表现的全开源 Web Agent。
1392 87
|
4天前
|
JavaScript Java 大数据
基于JavaWeb的销售管理系统设计系统
本系统基于Java、MySQL、Spring Boot与Vue.js技术,构建高效、可扩展的销售管理平台,实现客户、订单、数据可视化等全流程自动化管理,提升企业运营效率与决策能力。
|
5天前
|
人工智能 Java API
AI 超级智能体全栈项目阶段一:AI大模型概述、选型、项目初始化以及基于阿里云灵积模型 Qwen-Plus实现模型接入四种方式(SDK/HTTP/SpringAI/langchain4j)
本文介绍AI大模型的核心概念、分类及开发者学习路径,重点讲解如何选择与接入大模型。项目基于Spring Boot,使用阿里云灵积模型(Qwen-Plus),对比SDK、HTTP、Spring AI和LangChain4j四种接入方式,助力开发者高效构建AI应用。
283 122
AI 超级智能体全栈项目阶段一:AI大模型概述、选型、项目初始化以及基于阿里云灵积模型 Qwen-Plus实现模型接入四种方式(SDK/HTTP/SpringAI/langchain4j)
|
5天前
|
弹性计算 安全 数据安全/隐私保护
2025年阿里云域名备案流程(新手图文详细流程)
本文图文详解阿里云账号注册、服务器租赁、域名购买及备案全流程,涵盖企业实名认证、信息模板创建、域名备案提交与管局审核等关键步骤,助您快速完成网站上线前的准备工作。
232 82
2025年阿里云域名备案流程(新手图文详细流程)