【iOS】ARC内存管理

这篇具有很好参考价值的文章主要介绍了【iOS】ARC内存管理。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

内存管理的思考方式

怎么说呢。经典再放送咯。

对象操作 对应的Objective-C方法
生成并持有对象 alloc/new/copy/mutableCopy等方法
持有对象 retain方法
释放对象 release方法
废弃对象 dealloc方法

iOS底层内存管理方式

iOS内存管理方案有三种,我们详细看下每种方案的实现及存在的意义。

1. tagged pointer

标签指针

没有这种管理机制会引起内存浪费,为什么呢?

我们来看下,假设我们要存储一个NSNumber对象,其值是一个整数。正常情况下,如果这个整数只是一个NSInteger的普通变量,那么它所占用的内存是与CPU的位数有关,在32位CPU下占4个字节,在64位CPU下是占8个字节的。而指针类型的大小通常也是与CPU位数相关,一个指针所占用的内存在32位CPU下为4个字节,在64位CPU下也是8个字节。

所以一个普通的iOS程序,如果没有Tagged Pointer对象,从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种NSNumber、NSDate一类的对象所占用的内存会翻倍。如下图所示:

【iOS】ARC内存管理,ios,cocoa,macos

我们再来看看效率上的问题,为了存储和访问一个NSNumber对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。

为了改进上面提到的内存占用和效率问题,苹果提出了Tagged Pointer对象。由于NSNumberNSDate一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿(注:2^31=2147483648,另外1位作为符号位),对于绝大多数情况都是可以处理的。

所以我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了Tagged Pointer对象之后,64位CPU下NSNumber的内存图变成了以下这样:

【iOS】ARC内存管理,ios,cocoa,macos

当8字节可以承载用于表示的数值时,系统就会以Tagged Pointer的方式生成指针,如果8字节承载不了时,则又用以前的方式来生成普通的指针。以上是关于Tag Pointer的存储细节。
Tagged Pointer的特点:

  • 我们也可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:Tagged Pointer专门用来存储小的对象,例如NSNumberNSDate, 当然NSString小于60字节的也可以运用了该手段
  • Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已,因为他没有isa指针。所以,它的内存并不存储在堆中,也不需要mallocfree
  • 在内存读取上有着3倍的效率,创建时比以前快106倍。

由此可见,苹果引入Tagged Pointer,不但减少了64位机器下程序的内存占用,还提高了运行效率。完美地解决了小内存对象在存储和访问效率上的问题。

2. non-pointer iSA–非指针型iSA

在64位系统上只需要32位来储存内存地址,而剩下的32位就可以用来做其他的内存管理

non_pointer iSA 的判断条件:

1 : 包含swift代码;

2:sdk版本低于10.11;

3:runtime读取image时发现这个image包含__objc_rawi sa段;

4:开发者自己添加了OBJC_DISABLE_NONPOINTER_ISA=YES到环境变量中;

5:某些不能使用Non-pointer的类,GCD等;

6:父类关闭。

3. SideTables,RefcountMap,weak_table_t

为了管理所有对象的引用计数和weak指针,苹果创建了一个全局的SideTables,虽然名字后面有个"s"不过他其实是一个全局的Hash表,里面的内容装的都是SideTable结构体而已。它使用对象的内存地址当它的key。管理引用计数和weak指针就靠它了。

因为对象引用计数相关操作应该是原子性的。不然如果多个线程同时去写一个对象的引用计数,那就会造成数据错乱,失去了内存管理的意义。同时又因为内存中对象的数量是非常非常庞大的需要非常频繁的操作SideTables,所以不能对整个Hash表加锁。苹果采用了分离锁技术。

下边是SideTabel的定义:

SideTable
   struct SideTable {
     //锁
     spinlock_t slock;
     //强引用相关
     RefcountMap refcnts;
     //弱引用相关
     weak_table_t weak_table;
     ...
}

当我们通过SideTables[key]来得到SideTable的时候,SideTable的结构如下:

1、一把自旋锁。spinlock_t slock;

自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。

它的作用是在操作引用计数的时候对SideTable加锁,避免数据错误。

苹果在对锁的选择上可以说是精益求精。苹果知道对于引用计数的操作其实是非常快的。所以选择了虽然不是那么高级但是确实效率高的自旋锁。

2、引用计数器 RefcountMap * refcnts;

对象具体的引用计数数量是记录在这里的。

这里注意RefcountMap其实是个C++的Map。为什么Hash以后还需要个Map呢?因为内存中对象的数量实在是太庞大了我们通过第一个Hash表只是过滤了第一次,然后我们还需要再通过这个Map才能精确的定位到我们要找的对象的引用计数器。

引用计数器的数据类型是:

typedef __darwin_size_t size_t;

再进一步看它的定义其实是unsigned long,在32位和64位操作系统中,它分别占用32和64个bit。

苹果经常使用bit mask技术。这里也不例外。拿32位系统为例的话,可以理解成有32个盒子排成一排横着放在你面前。盒子里可以装0或者1两个数字。我们规定最后边的盒子是低位,左边的盒子是高位。

(1UL<<0)的意思是将一个"1"放到最右侧的盒子里,然后将这个"1"向左移动0位(就是原地不动):0b0000 0000 0000 0000 0000 0000 0000 0001

(1UL<<1)的意思是将一个"1"放到最右侧的盒子里,然后将这个"1"向左移动1位:0b0000 0000 0000 0000 0000 0000 0000 0010

下面来分析引用计数器(图中右侧)的结构,从低位到高位。

(1UL<<0)???WEAKLY_REFERENCED

表示是否有弱引用指向这个对象,如果有的话(值为1)在对象释放的时候需要把所有指向它的弱引用都变成nil(相当于其他语言的NULL),避免野指针错误。

(1UL<<1)???DEALLOCATING

表示对象是否正在被释放。1正在释放,0没有

(1UL<<(WORD_BITS-1))???SIDE_TABLE_RC_PINNED

其中WORD_BITS在32位和64位系统的时候分别等于32和64。其实这一位没啥具体意义,就是随着对象的引用计数不断变大。如果这一位都变成1了,就表示引用计数已经最大了不能再增加了。

3、维护weak指针的结构体 weak_table_t * weak_table;

第一层结构体中包含两个元素。

第一个元素weak_entry_t *weak_entries;是一个数组,上面RefcountMap是要通过find(key)来找到精确的元素的。weak_entries则是通过循环遍历来找到对应的entry

(上面管理引用计数器苹果使用的是Map,这里管理weak指针苹果使用的是数组,有兴趣的朋友可以思考一下为什么苹果会分别采用这两种不同的结构)

这个是因为weak的显著的特征来决定的: 当weak对象被销毁的时候,要把所有指向该对象的指针都设为nil。

第二个元素num_entries是用来维护保证数组始终有一个合适的size。比如数组中元素的数量超过3/4的时候将数组的大小乘以2。

第二层weak_entry_t的结构包含3个部分:

1、referent:被指对象的地址。前面循环遍历查找的时候就是判断目标地址是否和他相等。
2、referrers:可变数组,里面保存着所有指向这个对象的弱引用的地址。当这个对象被释放的时候,referrers里的所有指针都会被设置成nil。
3、inline_referrers只有4个元素的数组,默认情况下用它来存储弱引用的指针。当大于4个的时候使用referrers来存储指针。

上面我们介绍了苹果为了更好的内存管理使用的三种不同的内存管理方案,在内部采用了不同的数据结构以达到更高效内存检索。

内存管理有关修饰符

__strong修饰符

__strong修饰符是id类型和对象类型默认的所有权修饰符。

不论调用哪种方法,强引用修饰的变量会持有该对象,如果已经持有则引用计数不会增加。

对象的所有者和对象的生命周期

  1. 持有强引用的变量在超出其作用域时被废弃
  2. 随着强引用的失效,引用的对象会随之释放

__strong对象相互赋值

__strong修饰符的变量不仅只在变量作用域中,在赋值上也能够正确的管理其对象的所有者。

id __strong obj0 = [[NSObject alloc] init];//生成对象A			
id __strong obj1 = [[NSObject alloc] init];//生成对象B		
id __strong obj2 = nil;
obj0 = obj1;//obj0强引用对象B;而对象A不再被ojb0引用,被废弃
obj2 = obj0;//obj2强引用对象B(现在obj0,ojb1,obj2都强引用对象B)	
obj1 = nil;//obj1不再强引用对象B	
obj0 = nil;//obj0不再强引用对象B	
obj2 = nil;//obj2不再强引用对象B,不再有任何强引用引用对象B,对象B被废弃

方法参数中使用__strong

废弃Test对象的同时,Test对象的obj_成员变量也被废除
即成员变量的生存周期是与对象同步的。

__strong导致的循环引用

循环引用就是两个对象相互引用导致在该释放的时候没有释放,从而一直占着内存导致内存泄漏

内存泄漏:在内存该被释放的时候没有释放,导致内存被浪费使用了

举个例子:

#import <Foundation/Foundation.h>

@interface StrongTest : NSObject {
    id __strong _obj;
}
- (void) setObject:(id __strong) obj;

@end

#import "StrongTest.h"
@implementation StrongTest
- (void) setObject:(id)obj {
    _obj = obj;
}
@end

main函数:

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        StrongTest *testFirst = [[StrongTest alloc] init];//生成Test1
        StrongTest *testSecond = [[StrongTest alloc] init];//生成Test2
        
        [testFirst setObject:testSecond];
        [testSecond setObject:testFirst];
        
        //打印一下引用计数值
        NSLog(@"%lu", CFGetRetainCount((__bridge  CFTypeRef)testFirst));       
    }
    return 0;
}

我们创建了两个对象testFirsttestSecond
每个对象内部都有一个成员变量_obj
其中一个对象的_obj持有另一个对象

最后就会造成下面的结果:

testFirst持有Test1testFirst.obj持有Test2testSecond持有Test2testSecond.obj持有Test1

失效阶段的表示:

what happen Test1引用计数 Test1持有者 Test2引用计数 Test2持有者
初始状态 2 testFirst, testSecond.obj 2 testFirst.obj, testSecond
testFirst超出作用域 1 testSecond.obj 2 testFirst.obj, testSecond
testSecond超出作用域 1 testSecond.obj 1 testFirst.obj

这样一来,即使两个对象的指针都超出作用域,由于其中彼此的成员变量相互持有彼此的对象而造成循环引用。

__weak修饰符

__weak弱引用(引用计数不会加一 对象随时可能会被dealloc)
内部使用__autoreleasing来维持该对象不被dealloc(这个也是__weak修饰符修饰的对象在所指向的对象销毁之后会自动指向nil的关键所在)
对象的引用计数是记录在一张表上的,不在对象本身或者指针中,系统通过访问这张表来确定是否释放该对象。

将上面相互引用例子中的成员变量变为weak,即可避免循环引用。

weak还有个作用。在持有某对象的弱引用时,若该对象被废弃,则此若引用将自动失效且处于nil被赋值的状态(空弱引用)。

weak提供弱引用,弱引用不持有对象,NSObject对象会被销毁,所以会报一个警告

【iOS】ARC内存管理,ios,cocoa,macos

我们可以这样使用__weak修饰符的变量,将__strong修饰的对象赋值给__weak修饰的对象,这样就不会发生警告了:

	id __strong objTest = [[NSObject alloc] init];
    id __weak objTestSecond = objTest;

类似于“强弱共舞”

__weak修饰符的引用计数的问题

按以下例子,我们来看一下这两个的引用计数的值:

    id __strong obj = [[NSObject alloc] init];
    id __weak objTest = obj;
    NSLog(@"%lu", CFGetRetainCount((__bridge  CFTypeRef)obj));
    NSLog(@"%lu", CFGetRetainCount((__bridge  CFTypeRef)objTest));

按照常理来说应该是1 1,因为__weak修饰符不持有对象,引用计数值两个都应该是1

打印一下结果:

【iOS】ARC内存管理,ios,cocoa,macos

【iOS】ARC内存管理,ios,cocoa,macos

对比一下就能发现 两行NSLog的汇编代码并不一样

第二个__weak修饰符的NSLog在于开始先loadWeakRetained
然后在打印结束后有一个release操作

在打印__weak的引用计数时NSLog先将其以强引用,防止没有打印就释放掉了造成程序的崩溃,在NSLog结束时,会调用objc_release使引用计数减一。

__unsafe_unretained

__unsafe_unretained修饰符正如其名unsafe所示,是不安全的所有权修饰符。
附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。

为什么是不安全的呢

weak 修饰的指针变量,在指向的内存地址销毁后,会在 Runtime 的机制下,自动置为 nil。 _unsafe_unretain 不会置为 nil,容易出现 悬垂指针,发生崩溃。但是 __unsafe_unretain __weak 效率高。
悬垂指针 指针指向的内存已经被释放了,但是指针还存在,这就是一个 悬垂指针 或者说 迷途指针。野指针,没有进行初始化的指针,其实都是 野指针

附有__unsafe_unretained修饰符的变量同附有__weak修饰符的变量一样,生成的对象会立即释放。

在使用__unsafe_unretained修饰符时,赋值给附有__strong修饰符的变量时有必要确保被赋值的对象确实存在,如果不存在,那么程序就会崩溃。

__autoreleasing修饰符

与MRC进行比较:
MRC中autorelease的使用方法:

  1. 生成并持有NSAutoreleasePool对象
  2. 调用已分配对象的autorelease方法(将对象注册到pool中)
  3. 废弃NSAutoreleasePool对象

在ARC环境下:

NSAutoreleasePool *pool = [NSAutoreleasePool alloc] init]; 1⃣️
[obj autorelease]; 2⃣️
[pool drain]; 3⃣️

等价于:

@autoreleasepool{ 1⃣️
id __autorelease obj2; obj2 = obj; 2⃣️
} 3⃣️

自动调用:

编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是将自动把返回值的对象注册到autoreleasepool

下面情况不使用__autoreleasing修饰符也能使对象注册到autoreleasepool中。

+ (id) array {
	return [[NSMutableArray alloc]init];
}

//如下:
+ (id) array {
	id obj = [[NSMutableArray alloc]init];
	return obj;
}

由于return使得对象变量超出其作用域,所以该强引用对应的自己持有的对象会被自动释放,但该对象作为函数的返回值,编译器也会自动注册到自动释放池

自动调用时的失效过程:

随着obj超出其作用域,强引用失效,所以自动释放自己持有的对象。
同时,随着@autoreleasepool块的结束,注册到autoreleasepool中的所有对象被自动释放。 因为对象的拥有者不存在,所以废弃对象。

weak修饰符与autoreleasing修饰符:

为什么访问__weak修饰符的变量时必须访问注册到autoreleasepool的对象呢,这是因为__weak修饰符只持有对象的弱引用,而在访问引用对象的过程中,该对象有可能被废弃。如果把要访问的对象注册到autoreleasepool中,那么在@autoreleasepool块结束之前都能确保该对象存在,因此,在使用附有__weak修饰符的变量时就必定要使用注册到autoreleasepool中的对象。

id __weak obj1 = obj0;
NSLog(@"class = %@", [obj1 class]);
与以下源代码相同
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class = %@", [temp class]);

例1

	//这个例子中obj0没有加入到自动释放池中
    id  obj0 = [[NSObject alloc] init];
    id __weak obj1 = obj0;
    NSLog(@"class = %@", [obj1 class]);

【iOS】ARC内存管理,ios,cocoa,macos

池中没有出现obj0。

例2

	//这个例子中obj0加入了自动释放池中
    id __autoreleasing obj0 = [[NSObject alloc] init];
    id __weak obj1 = obj0;
    NSLog(@"class = %@", [obj1 class]);

【iOS】ARC内存管理,ios,cocoa,macos

池中出现了obj0

例3

    id __autoreleasing obj0 = [[NSObject alloc] init];
    id __weak obj1 = obj0;
    id __autoreleasing obj2 = obj1;
    NSLog(@"class = %@", [obj2 class]);

【iOS】ARC内存管理,ios,cocoa,macos

池中出现了obj0,且看到count为2

例4

    id  obj0 = [[NSObject alloc] init];
    id __weak obj1 = obj0;
    id __autoreleasing obj2 = obj1;
    NSLog(@"class = %@", [obj2 class]);

【iOS】ARC内存管理,ios,cocoa,macos

池中出现了obj0

总结: __weak修饰符并不会将对象加入到自动释放池,但是我们使用__weak修饰的对象一定要是本身已经加入到自动释放池的或者后续使用__autoreleasing将__weak所修饰的对象加入释放池

具体ARC规则:

  • 不能使用retain/release/retainCount/autorelease
  • 不能使用NSAllocateObject/NSDeallocateObject
  • 必须遵守内存管理的方法名规则
  • 不要显式调用dealloc
  • 使用@autorelease块代替NSAutoreleasePool
  • 不能使用区域(NSZone)
  • 对象型变量不能作为C语言结构体的成员
  • 显式转换id和void*

重点:

不能显式调用dealloc

dealloc无法释放不属于该对象的一些东西,需要我们重写时加上去,例如:文章来源地址https://www.toymoban.com/news/detail-567706.html

  • 通知的观察者,或KVO的观察者
  • 对象强委托/引用的解除(例如XMPPMannerger的delegateQueue)
  • 做一些其他的注销之类的操作(关闭程序运行期间没有关闭的资源)
In the implementation of dealloc, do not call the implementation of superclass. You should try to avoid using dealloc to manage the lifetime of limited resources, such as file descriptors.
You never send a dealloc message directly. Instead, the dealloc method of the object is called by the runtime.
在dealloc的实现中,不要调用超类的实现。您应该尽量避免使用dealloc管理有限资源(如文件描述符)的生存期。
不要直接发送dealloc消息。与直接发送dealloc消息不同,对象的dealloc方法由运行时调用。

Special Considerations
When not using ARC, your implementation of dealloc must invoke the superclass’s implementation as its last instruction.
特别注意事项
当不使用ARC时,dealloc的实现必须调用父类(super)的实现作为它的最后一条指令 [super dealloc]

__bridge

  • bridge可以实现Objective-C与C语言变量和Objective-C与CoreFoundation对象之间的互相转换
  • _bridge不会改变对象的持有状况,既不会retain,也不会release
  • _bridge转换需要慎重分析对象的持有情况,稍不注意就会内存泄漏
  • _bridge_retained用于将口C变量转换为C语言变量或将OC对象转换为CoreFoundation对象
  • _bridge_retained类似于retain,“被转换的变量”所持有的对象在变量赋值给"转换目标变量“后持有该对象
  • _bridge_transfer用于将C语言变量转换为OC变量或将CoreFoundation对象转换为OC对象
  • bridge_transfer类似于release,“被转换的变量”所持有的对象在变量赋值给“转换目标变量”后随衣释热

属性关键字和所有权修饰符

属性关键字 所有权修饰符
assign _unsafe_unretained
copy __strong
retain __strong
strong __strong
__unsafe_unretained __unsafe_unretained
weak __weak

到了这里,关于【iOS】ARC内存管理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【iOS内存管理-内存的几大区域】

    iOS内存管理的第一篇章,了解iOS内存的五大分区。 iOS中,内存主要分为五大区域:栈区,堆区,全局区/静态区,常量区和代码区。总览图如下。 如上图所示,代码区是在低地址段存放,而栈区则存放在高地址段,并且各个分区之间不是连续的。 栈是 从高地址向低地址存储

    2024年02月16日
    浏览(34)
  • 【iOS内存管理-编译链接的过程】

    就我而言,iOS开发的过程中接触到的编译链接方面的知识很少,这部分知识还是很重要的。 对于iOS的编译链接过程来说并不难,和微机原理的汇编过程还是挺像的。今天对于编译链接的过程学习和了解一下。 参考:iOS程序员的自我修养-编译、链接过程 参考:iOS编译过程 计

    2024年02月16日
    浏览(41)
  • 初识FreeRTOS入门,对FreeRTOS简介、任务调度、内存管理、通信机制以及IO操作,控制两个led不同频率闪烁

    当代嵌入式系统的开发越来越复杂,实时性要求也越来越高。为了满足这些需求,开发者需要使用实时操作系统(RTOS),其中一个流行的选择是FreeRTOS(Free Real-Time Operating System)。本篇博客将详细介绍FreeRTOS的特性、任务调度、内存管理、通信机制以及一些示例代码。 FreeR

    2024年02月14日
    浏览(37)
  • macos编译libtiff库给IOS用

         

    2024年02月12日
    浏览(43)
  • Charles证书过期解决方法macos/ios

    今天心血来潮打开Charles想试试看抓包手机APP(ios),结果发现各种x和提示ssl错误。开始以为是和魔法的代理冲突或者ip变了,捯饬很久后发现web的也报错。 然后搜了一会原因发现时证书过期了 1、搜索“钥匙串访问”,直接搜索“charles”,找到打叉的名称,直接删掉 2、打开

    2024年02月03日
    浏览(52)
  • iOS/macOS - 逐行写入文件 (NSFileHandle)

    2024年02月15日
    浏览(46)
  • MacOS 14 系统 XCode15、 Flutter 开发 IOS

    MacOS14 Sonoma 安装 Flutter 开发环境 MacOS 系统 Flutter开发Android 环境配置 MacOS 系统 Flutter开发IOS 环境配置​​​​​​​ 前面我们已经在MacOS14 M3芯片上安装好 Flutter环境,包括开发工具 VsCode 、Android Stuiod,那么flutter如何开发IOS呢? 我们知道IOS开发语言为 objcet-c或者 swift,Flutter是

    2024年02月03日
    浏览(89)
  • macOS Sonoma编译OpenCV源码输出IOS平台库

    1.macOS下载并编译OpenCV源码:  克隆源码: 主仓: git clone https://github.com/opencv/opencv.git 扩展仓:  git clone https://github.com/opencv/opencv_contrib.git    编译xcode源码需要CMake与XCode命令行工具 确认已安装CMake  确认已安装XCode  安装xcode command line tools 确认系统已安装python环境

    2024年02月10日
    浏览(62)
  • uniapp打包之配置MacOS虚拟机生成iOS打包证书

    uniapp是一款跨端开发框架,可用于快速开发iOS、Android、H5等多端应用。本文将详细介绍如何实现uniapp开发的iOS应用打包。 一、下载苹果原版镜像文件 点击此处下载 二、安装VMware uniapp打包iOS应用需要生成相应证书和P2文件,这些都需要用到IOS环境,这里我是使用的是MacOS虚拟机

    2024年02月12日
    浏览(56)
  • macos搭建appium-iOS自动化测试环境

    目录 准备工作 安装必需的软件 安装appium 安装XCode 下载WDA工程 配置WDA工程 搭建appium+wda自动化环境 第一步:启动通过xcodebuild命令启动wda服务 分享一下如何在mac电脑上搭建一个完整的appium自动化测试环境 前期需要准备的设备和账号: mac电脑一台 iphone一台 苹果开发者账号一

    2024年02月13日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包