【iOS】——分类拓展关联对象

分类

OC的动态特征允许使用类别为现有的类添加新方法并且不需要创建子类,不需要访问原有类的源代码。通过使用类别即可动态为现有的类添加新方法,而且可以将类定义模块化分布到多个相关文件。

  • 分类是 Objective-C 中的一种语言特性,它允许你为现有类添加新的方法,而无需修改原始类的源代码。
  • 分类就像一个“扩展”,你可以用它来添加新的功能,而不必创建子类。
  • 分类不能添加新的实例变量,只能添加方法。
  • 分类也可以把framework私有方法公开化,通过在分类中声明类的实现部分的私有方法即可通过分类来调用类中的某个私有方法
  • 分类中添加的属性并没有自动生成成员变量,也没有实现set和get方法,只是生成了set和get方法的声明

示例代码如下:

@interface NSString (MyCategory)
- (NSString *)reverseString;
@end@implementation NSString (MyCategory)
- (NSString *)reverseString {NSMutableString *reversedString = [[NSMutableString alloc] init];for (NSInteger i = self.length - 1; i >= 0; i--) {[reversedString appendString:[self substringWithRange:NSMakeRange(i, 1)]];}return reversedString;
}
@end

分类应用场景:

  • 扩展系统类功能: 例如,为 NSString 类添加 reverseString 方法来反转字符串。
  • 模块化代码: 将相关的方法分组到分类中,例如将所有与网络相关的操作方法放到一个名为 Networking 的分类中。
  • 扩展第三方库功能: 为第三方库的类添加新方法,例如为 AFNetworking 添加一个方法来处理特定类型的 API 请求。
  • 实现协议方法: 分类可以用来实现协议方法,而无需子类化。
  • 委托模式: 分类可以用来实现委托方法,而无需子类化。
  • 延迟加载: 分类可以用来实现延迟加载,例如将一些耗时的操作放到分类方法中,并在需要时才加载。

分类的定义如下:

struct category_t {// 分类名称const char *name;// 分类所属的类classref_t cls;// 实例方法列表struct method_list_t *instanceMethods;// 类方法列表struct method_list_t *classMethods;// 协议列表struct protocol_list_t *protocols;// 实例属性列表struct property_list_t *instanceProperties;// 类属性列表(可能不存在)struct property_list_t *_classProperties;// 获取实例方法或类方法列表method_list_t *methodsForMeta(bool isMeta) {if (isMeta) return classMethods;else return instanceMethods;}// 获取实例属性或类属性列表property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

从结构体可以看出,分类能

  • 给类添加实例方法 (instanceMethod)
  • 给类添加类方法 (classMethod)
  • 实现协议 (protocol)
  • 添加属性 (instancePropertie)

但是不能添加实例变量,即无法自动生成实例变量的setter和getter方法

扩展

扩展与类别相似,有时候被称为匿名分类。但是两者实质上不是一个内容。
扩展是在编译阶段与该类同时编译的,是类的一部分。扩展中声明的方法只能在该类的@implementation中实现。所以这也就意味着我们无法对系统的类使用扩展。
同时与分类不同,扩展不但可以声明方法,还可以声明成员变量,这是分类所做不到的。

示例代码如下:

#import "Car1.h"
@interface Car1 ()
@property (nonatomic, copy) NSString *color;
- (void)drive:(NSString *)owner;
@end#import "Car1.h"
#import "Car1+drive.h"@implementation Car1- (void)drive {NSLog (@"%@汽车在路上跑", self);
}
- (void)drive:(NSString*)owner {NSLog(@"%@正在驾驶%@汽车在路上跑", owner, self);
}
- (NSString*)description {return [NSString stringWithFormat:@"<Car[_brand = %@, _model = %@, _color = %@]>",self.brand, self.model, self.color];
}
@end
两者区别
  • 分类原则上只能增加方法,但是也可以通过关联属性增加属性
  • 拓展可以增加方法和成员变量,都是私有的,实现部分在类中。
  • 扩展只能在自身类中使用,而不是子类或者其他地方。
  • 扩展是在编译阶段添加到类中,而分类是在运行时添加到类中

关联对象

关联对象允许你为一个对象添加额外的属性,即使这个对象本身没有定义这些属性。

因此,可以使用关联对象给分类添加属性

下面是关联对象的API

//添加关联对象
void objc_setAssociatedObject(id object, const void * key, id value, objc AssociationPolicy policy)//获得关联对象
id objc_getAssociatedObject(id object, const void * key)//移除所有的关联对象
void objic_removeAssociatedObjects(id object)
  • object: 要添加属性的对象。
  • key: 用于标识属性的键,通常使用一个 NSString 对象。
  • value: 要添加的属性值。
  • policy: 关联策略,用于指定属性的生命周期和访问权限。

关联策略如下:

typedef OBJC ENUM(uintptr_t, objc_AssociationPolicy) {OBJC_ ASSOCIATION_ASSIGN = 0, //指定个弱引用相关联的对象OBJC_ ASSOCIATION_RETAIN_NONATOMIC = 1; //指定相关对象的强引用, 非原子性OBJC_ ASSOCIATION_COPY_NONATOMIC = 3; //指定相关的对象被复制, 非原子性OBJC_ ASSOCIATION_RETAIN = 01401; //指定相关对象的强引用,原子性OBJC_ ASSOCIATION_COPY = 01403; //指定相关的对象被复制, 原子性
};

给key设置值一般来说有三种方法

  1. 针对每个属性,定义-个全局的key名, 然后取其地址,这一定是唯一的加上static,只在文件内部有效
static const void *NameKey = &NameKey;
static const void *WeightKey = &WeightKey;
  1. 针对每个属性,因为类中的属性名是唯一的,直接拿属性名作为key
#define NameKey = @"name";
#define WeightKey = @"weight";
  1. 使用@selector作为key
 @selector(name)//直接用属性名对应的get方法的selector,有提示不容易写错。并且get方法隐藏参数cmd 可以直接用,看上去就会更加简洁

下面是示例代码:

#import <UIKit/UIKit.h>
#import "objc/runtime.h"
NS_ASSUME_NONNULL_BEGIN@interface UIView (defaultColor)
@property (nonatomic, strong)UIColor* defaultColor;
@endNS_ASSUME_NONNULL_END#import "UIView+defaultColor.h"@implementation UIView (defaultColor)@dynamic defaultColor;static char kDefaultColorKey;- (void)setDefaultColor:(UIColor *)defaultColor {//objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);objc_setAssociatedObject(self, @selector(defaultColor), defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}- (id)defaultColor {//return objc_getAssociatedObject(self, &kDefaultColorKey);return objc_getAssociatedObject(self, _cmd);
}
@end

通过上面的关联对象我们就给系统提供的UIView设置了默认颜色的属性

在这里插入图片描述

_cmd 是一个特殊的参数,它代表当前正在执行的方法的选择器(selector)。选择器是一个字符串,它标识了方法的名称。

_cmd 可以让方法在执行过程中获取自身的信息,例如方法名称

关联对象底层探索

实现关联对象技术的核心对象有

  1. AssociationsManager
  2. AssociationsHashMap
  3. ObjectAssociationMap
  4. ObjcAssociation

objc_setAssociatedObject

首先来看创建关联对象的函数,源码如下:

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{//isa有一位信息为禁止关联对象,如果设置了,直接报错if (!object && !value) return;// 判断runtime版本是否支持关联对象if (object->getIsa()->forbidsAssociatedObjects())_objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));// 将 object 封装成 DisguisedPtr 目的是方便底层统一处理DisguisedPtr<objc_object> disguised{(objc_object *)object};// 将 policy和value 封装成ObjcAssociation,目的是方便底层统一处理ObjcAssociation association{policy, value};// (如果有新值)保留锁外的新值。// retain the new value (if any) outside the lock.// 根据传入的缓存策略,创建一个新的value对象association.acquireValue();bool isFirstAssociation = false;{//调用构造函数,构造函数内加锁操作AssociationsManager manager; // 创建一个管理对象管理单例,类AssociationsManager管理一个锁/哈希表单例对。分配一个实例将获得锁// 并不是全场唯一,构造函数中加锁只是为了避免重复创建,在这里是可以初始化多个AssociationsManager变量的//获取全局的HasMap// 全场唯一AssociationsHashMap &associations(manager.get());if (value) {//去关联表中找对象对应的关联对象表,如果没有内部会重新生成一个auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});//如果没有找到if (refs_result.second) {/* it's the first association we make */// 这是我们建立的第一个关联//说明是第一次设置关联对象,把是否关联对象设置为YESisFirstAssociation = true;}// 建立或替换关联/* establish or replace the association */// 获取ObjectAssociationMap中存储值的地址auto &refs = refs_result.first->second;// 移除之前的关联,根据key// 将需要存储的值存放在关联表中存储值的地址中// 同时会根据key去查找,如果查找到`result.second` = false ,如果找不到就创建`result.second` = true// 创建association时,当(association的个数+1)超过3/4,就会进行两倍扩容auto result = refs.try_emplace(key, std::move(association));if (!result.second) {// 交换association和查询到的`association`// 其实可以理解为更新查询到的`association`数据,新值替换旧值association.swap(result.first->second);}} else {// 这里相当于传入的nil,移除之前的关联// 到AssociationsHashMap找到ObjectAssociationMap,将传入key对应的值变为空。// 查找disguised 对应的ObjectAssociationMapauto refs_it = associations.find(disguised);// 如果找到对应的 ObjectAssociationMap 对象关联表if (refs_it != associations.end()) {// 获取 refs_it->second 里面存放了association类型数据auto &refs = refs_it->second;// 根据key查询对应的associationauto it = refs.find(key);if (it != refs.end()) {// 如果找到,更新旧的association里面的值association.swap(it->second);refs.erase(it);if (refs.size() == 0) {// 如果该对象关联表中所有的关联属性数据被清空,那么该对象关联表会被释放associations.erase(refs_it);}}}}}// 在锁外面调用setHasAssociatedObjects,因为如果对象有一个,这个//将调用对象的noteAssociatedObjects方法,这可能会触发initialize,这可能会做任意的事情,包括设置更多的关联对象。if (isFirstAssociation)object->setHasAssociatedObjects();// release the old value (outside of the lock).// 释放旧的值(在锁外部)association.releaseHeldValue();
}
  • 首先就是进行参数检查和进行安全处理(检查 objectvalue 是否都为 nil,检查 object 的类是否允许关联对象)
  • 接着封装数据类型,便于底层处理(将 object 封装成 DisguisedPtr<objc_object> 类型,将 policyvalue 封装成 ObjcAssociation 类型)
  • 接着使用 association.acquireValue() 保留新值,确保新值不会被释放
  • 然后创建一个 AssociationsManager 对象,并通过它来获取全局的 AssociationsHashMap 对象
  • AssociationsHashMap 中查找 object 对应的关联表 ObjectAssociationMap
  • 如果没有找到ObjectAssociationMap,则创建一个新的 ObjectAssociationMap,并将其插入到 AssociationsHashMap 中。
  • 如果找到ObjectAssociationMap,则在ObjectAssociationMap接着查找 key 对应的关联信息
  • 如果找到,则使用新值替换旧值。
  • 如果没有找到,则创建一个新的关联信息,并将新值存储在 ObjectAssociationMap 中。
  • 如果 valuenil,则表示要移除关联。系统会查找 ObjectAssociationMapkey 对应的关联信息,并将其移除。

objc_getAssociatedObject

获取关联对象的源码如下:

id
_object_get_associative_reference(id object, const void *key)
{ObjcAssociation association{};//创建空的关联对象{AssociationsManager manager;//创建一个AssociationsManager管理类AssociationsHashMap &associations(manager.get());//获取全局唯一的静态哈希mapAssociationsHashMap::iterator i = associations.find((objc_object *)object);//找到迭代器,即获取bucketsif (i != associations.end()) {//如果这个迭代查询器不是最后一个 获取ObjectAssociationMap &refs = i->second; //找到ObjectAssociationMap的迭代查询器获取一个经过属性修饰符修饰的valueObjectAssociationMap::iterator j = refs.find(key);//根据key查找ObjectAssociationMap,即获取bucketif (j != refs.end()) {association = j->second;//获取ObjcAssociationassociation.retainReturnedValue();}}}return association.autoreleaseReturnedValue();//返回value
}
  • 首先创建AssociationsManager对象,接着通过它来获取全局的AssociationsHashMap
  • AssociationsHashMap 中查找object对应的ObjectAssociationMap
  • 如果找到ObjectAssociationMap,则在ObjectAssociationMap中接着查找key对应的关联信息并赋值给value
  • 最后返回value

objc_removeAssociatedObjects

移除关联对象的源码如下:


// 与设置/获取关联引用不同,此函数对性能敏感,因为原始isa对象(如OS对象)不能跟踪它们是否有关联对象。
void
_object_remove_assocations(id object, bool deallocating)
{ObjectAssociationMap refs{};{AssociationsManager manager;AssociationsHashMap &associations(manager.get());AssociationsHashMap::iterator i = associations.find((objc_object *)object);if (i != associations.end()) {refs.swap(i->second);// If we are not deallocating, then SYSTEM_OBJECT associations are preserved.//如果我们没有回收,那么SYSTEM_OBJECT关联会被保留。bool didReInsert = false;if (!deallocating) {for (auto &ref: refs) {if (ref.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {i->second.insert(ref);didReInsert = true;}}}if (!didReInsert)associations.erase(i);}}// Associations to be released after the normal ones.// 在正常关联之后释放关联。SmallVector<ObjcAssociation *, 4> laterRefs;// release everything (outside of the lock).// 释放锁外的所有内容。for (auto &i: refs) {if (i.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {// If we are not deallocating, then RELEASE_LATER associations don't get released.//如果我们不是在释放,那么RELEASE_LATER关联不会被释放if (deallocating)laterRefs.append(&i.second);} else {i.second.releaseHeldValue();}}for (auto *later: laterRefs) {later->releaseHeldValue();}
}
  • 首先使用 AssociationsManager 获取全局的 AssociationsHashMap,并查找 object 对应的关联表 ObjectAssociationMap
  • 如果找到关联表,则将其复制到 refs 变量中,以便在锁外进行操作。
  • 如果对象正在被释放,则所有关联对象都会被移除,但如果对象只是被修改,则系统关联对象会被保留
  • 如果没有保留任何关联对象,则从 AssociationsHashMap 中移除 object 对应的关联表
  • 遍历 refs 中的所有关联对象,并根据关联对象的策略进行释放。
  • 如果关联对象的策略包含 OBJC_ASSOCIATION_SYSTEM_OBJECT,则将其添加到 laterRefs 列表中,以便在所有其他关联对象释放后进行释放。
  • 如果关联对象的策略不包含 OBJC_ASSOCIATION_SYSTEM_OBJECT,则立即释放关联对象。
  • 最后遍历 laterRefs 中的所有关联对象,并释放它们

总结

  • 分类可以用来为类动态的添加方法,通过关联对象还能动态添加属性
  • 分类默认只能声明属性不会生成成员变量和对应的get和set方法
  • 分类也用于模块化设计
  • 分类是在运行期生成,扩展是类的一部分在编译期生成
  • 关联对象的API的实现都是通过操作AssociationsManager、AssociationsHashMap、ObjectAssociationMap、ObjcAssociation来实现

Category的方法会“覆盖”掉原来类的同名方法?

  • Category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果Category和原来类都有methodA,那么Category附加完成之后,类的方法列表里会有两个methodA

  • Category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的Category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就返回了,不会在理会后面的同名方法

关联对象被存储在什么地方,是不是存放在被关联对象本身的内存中?

关联对象存放在名为ObjectAssociationMap的哈希表中,存放关联对象的哈希表又被存放在名为AssociationsHashMap的哈希表中,通过AssociationsManager来管理。也就是说所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址,而这个map的value又是另外一个全局map,里面保存了关联对象的key。

关联对象的生命周期是怎样的,什么时候被释放,什么时候被移除?

关联对象的释放时机与移除时机并不总是一致。关联对象的生命周期取决于关联策略和目标对象的生存期。弱引用策略的关联对象会随着目标对象的释放而被释放,而强引用策略和复制引用策略的关联对象会继续存在,直到它们的引用计数降为 0。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1523621.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

缓解webclient频繁报‘Connection prematurely closed BEFORE response’的问题

现象&#xff1a; 我在Java代码中使用org.springframework.web.reactive.function.client.WebClient进行网络请求&#xff0c;一开始会有比较多的偶发报错&#xff1a;Connection prematurely closed BEFORE response&#xff0c;网络连接莫名其妙就断了。 处理&#xff1a; …

pm2 + linux + nginx

pm2 pm2是一个用于管理node项目的工具 前言 有如下两个文件 index.js const express require("express"); const app express(); const port 9999;app.get("/index", (req, res) > {res.json({code:200,msg:"songzx001"}) });app.lis…

学习硬件测试06:IIC(SHT30)+HMI串口屏+RS485(modbus)+SPI Flash读写+CAN通信(P81、P91、P95、P120、)

文章以下内容全部为硬件相关知识&#xff0c;鲜有软件知识&#xff0c;并且记的是自己需要的部分&#xff0c;大家可能看不明白。 一、IIC&#xff08;SHT30 数字温湿度传感器&#xff09; 1.1实验现象 1、软件模拟 I2C 协议与 SHT30 数字温湿度传感器通讯&#xff1b; &am…

怎么把视频转换成mp4:好用的mp4格式转换器免费版推荐

用手机或者其他拍摄设备记录生活已经成为一种日常&#xff0c;当你想把手机里储存的日常小确幸发布到平台上时&#xff0c;才发现你视频的格式在平台上并不被支持。这个事实难免让人丧气。如果你还想继续上传视频的话&#xff0c;就不得不把视频格式转换成被平台支持的mp4格式。…

ELK系列之一---探索ELK奇妙世界:初识日志界大名鼎鼎的ES集群!

目录 一、为什么要使用ELK 二、ELK简介 三、Elaticsearch入门 3.1、什么是elaticsearch 3.2、elaticsearch的底层优点 3.2.1、全文检索 3.2.2、倒排索引 3.3、elaticsearch集群原理 一、为什么要使用ELK 一般我们需要进行日志分析场景&#xff1a;直接在日志文件中 gre…

Redis从入门到入门(上)

1.Redis概述 文章目录 1.Redis概述1.1 什么是Redis1.2 Redis的应用场景 2.Linux下Redis的安装与使用2.1 Redis下载2.2 Redis的启动2.3 Redis配置2.4 连接Redis 1.1 什么是Redis Redis是用C语言开发的一个开源的高性能键值对&#xff08;key-value&#xff09;数据库&#xff0…

C语言sprintf函数使用

1 其函数原型为&#xff1a;int sprintf(char *str, const char *format,...)。 具体用法如下&#xff1a; 基本语法&#xff1a; str&#xff1a;目标字符串的指针&#xff0c;用于存储格式化后的结果。format&#xff1a;格式化字符串&#xff0c;用于指定输出的格式。后续是…

数据结构-队列的介绍及循环队列

1.队列的概念 在开始前&#xff0c;请牢记这句话&#xff1a;队列是一个先进先出的数据结构。 队列&#xff08;queue&#xff09;是限定在表的一端进行插入&#xff0c;表的另一端进行删除的数据结构&#xff0c;如同栈的学习&#xff0c;请联系前文所学链表&#xff0c;试想…

4.5SQL注入之加解密注入

SQL注入之加解密注入Base64是网络上最常见的用于传输8Bit字节码的编码方式之一&#xff0c;Base64就是一种基于64个可打印字符来表示二进制数据的方法。 Less-21关 Cookie加密注入&#xff1a; 通过Burpsuite抓包&#xff1a; 进行Base64解密&#xff1a;

波场(Tron)监听区块交易(TRX,USDT)

前言说明&#xff1a; 本篇文章参考GitHub一位伙伴的代码&#xff0c;再代码基础上优化改良以后的结果&#xff0c;但是一下找不到那位大佬的GitHub链接了&#xff0c;如有侵权请联系作者调整文章&#xff0c;让跟多人收益。谢谢。 实现思路: 波场链是一条很新奇的链&#xff…

Nexus配置npm私服

1&#xff0c;配置npm-hub 2&#xff0c;配置proxy-npm 3&#xff0c;配置group-npm 4&#xff0c;配置local-npm 5&#xff0c;配置淘宝

[overleaf] 论文中含有中文字符导致编译失败

解决方案分为两步&#xff1a; 1. 加入package&#xff1a; UTF8或者xeCJK \usepackage[UTF8]{ctex}二选一 \usepackage{xeCJK} 2. 修改编译方式&#xff1a; Menu -> Setting -> Compiler -> XeLatex

MyPrint打印设计器(七)svg篇-二阶贝塞尔曲线

svg-二阶贝塞尔曲线 介绍一款强大的svg操作库&#xff0c;能够通过简单的代码&#xff0c;实现svg绘制与操纵&#xff0c;实现拖拽等功能 代码仓库 在线体验 代码仓库&#xff1a;github 代码仓库&#xff1a;gitee 实战项目&#xff1a;MyPrint 操作简单&#xff0c;组件丰富…

Ovirt-Engine(4.3.10 )备份恢复

介绍如何进行 oVirt Engine 的备份、恢复以及相关操作&#xff0c;包括自动备份脚本、手动备份步骤、托管引擎的恢复流程&#xff0c;以及恢复后的配置和验证步骤。 1. Engine 备份部分 1.1 备份使用的脚本 以下是一个用于自动备份 oVirt Engine 的 Bash 脚本&#xff1a; …

标签中的ref属性

之前说过了 ref() 函数&#xff0c;现在说的标签中的 ref 属性 和 ref() 函数也存在一定关联。 2、 标签中的 ref 属性分为两种情况&#xff1a; 用在普通DOM标签上&#xff0c;获取的是DOM节点。 用在组件标签上&#xff0c;获取的是组件实例对象 Vue2 中标签上的 ref 属性…

掌握AIGC的魔法:编写高质量提示词的艺术与科学

嘿&#xff0c;技术达人们&#xff0c;&#x1f680; 今天我们来聊聊AIGC界的超级明星——提示词&#xff08;Prompt&#xff09;。在AI生成内容的奇妙世界里&#xff0c;提示词就是那个点石成金的魔法棒。想要AI小伙伴听你的指挥&#xff0c;创造出令人惊叹的内容吗&#xff1…

9.2~9.3-模型量化学习内容

量化简介 量化是将模型浮点数变为定点数运行的过程。通过一个原始float数值range(scale、min、max)&#xff0c;将类似实属域的float数值映射到一个网格比较稀疏的int网络上&#xff0c;中间肯定会产生数值的偏移。 基本概念 &#xff1a;模型量化可以减少模型尺寸&#xff0…

驾驶模拟左拐右拐

目录 根据4个点确定投影变换关系&#xff1a; 驾驶模拟左拐右拐 平移 四个点选 通过3个点定义放射变换&#xff1a;结果不对 根据4个点确定投影变换关系&#xff1a; import cv2 import numpy as npdef apply_perspective_transform(image, src_points, dst_points):# 将选…

spring--小白面试版01

bean 1.Spring框架中的bean是单例的吗? Service Scope("singleton") public class UserServicelmpl implements UserService { } 在Scope中 singleton: bean在每个Spring IOC容器中只有一个实例 prototype:一个bean的定义可以有多个实例 2. Spring框架中的单例bea…

jdk11安装步骤(含安装包)

安装包 通过百度网盘分享的文件&#xff1a;jdk-11.0.15.1_windows-x64_bin.exe 链接&#xff1a;https://pan.baidu.com/s/1IYRnvtPvfgloS8rawtRDvg 提取码&#xff1a;sv1w 一、安装过程 双击安装程序 二、配置环境 右键“此电脑”&#xff0c;点击“属性”&#xff0c;点…