iOS开发者对引用计数这个名词肯定不陌生,引用计数是苹果为了方便开发者管理内存而引入的一个概念,当引用计数为0时,对象就会被释放。但是,真的是所有对象都是这样吗?

内存分配

iOS将虚拟内存按照地址由低到高划分为如下五个区:

在程序运行时,代码区,常量区以及全局静态区的大小是固定的,会变化的只有栈和堆的大小。而栈的内存是有操作系统自动释放的,我们平常说所的iOS内存引用计数,其实是就堆上的对象来说的。

如何引入tagged pointer

自2013年苹果推出iphone5s之后,iOS的寻址空间扩大到了64位。我们可以用63位来表示一个数字(一位做符号位)。那么这个数字的范围是2^63 ,很明显我们一般不会用到这么大的数字,那么在我们定义一个数字时NSNumber *num = @100,实际上内存中浪费了很多的内存空间。

当然苹果肯定也认识到了这个问题,于是就引入了tagged pointer,tagged pointer是一种特殊的“指针”,其特殊在于,其实它存储的并不是地址,而是真实的数据和一些附加的信息。

我们可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:

  • Tagged Pointer专门用来存储小的对象,例如NSNumber, NSDate, NSString。
  • Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
  • 在内存读取上有着3倍的效率,创建时比以前快106倍。

NSTaggedPointer

我们先看下下面这段代码:

    NSMutableString *mutableStr = [NSMutableString string];NSString *immutable = nil;#define _OBJC_TAG_MASK (1UL<<63)char c = 'a';do {[mutableStr appendFormat:@"%c", c++];immutable = [mutableStr copy];NSLog(@"%p %@ %@", immutable, immutable, immutable.class);}while(((uintptr_t)immutable & _OBJC_TAG_MASK) == _OBJC_TAG_MASK);

运行结果:

2020-08-08 14:15:54.480862+0800 TaggedPointerDemo[55468:2078125] 0xdc5050684e86e57c a NSTaggedPointerString
2020-08-08 14:15:54.481719+0800 TaggedPointerDemo[55468:2078125] 0xdc5050684e80c57f ab NSTaggedPointerString
2020-08-08 14:15:54.482480+0800 TaggedPointerDemo[55468:2078125] 0xdc50506848b0c57e abc NSTaggedPointerString
2020-08-08 14:15:54.483342+0800 TaggedPointerDemo[55468:2078125] 0xdc50506e08b0c579 abcd NSTaggedPointerString
2020-08-08 14:15:54.483950+0800 TaggedPointerDemo[55468:2078125] 0xdc50563e08b0c578 abcde NSTaggedPointerString
2020-08-08 14:15:54.484246+0800 TaggedPointerDemo[55468:2078125] 0xdc56363e08b0c57b abcdef NSTaggedPointerString
2020-08-08 14:15:54.484800+0800 TaggedPointerDemo[55468:2078125] 0xda26363e08b0c57a abcdefg NSTaggedPointerString
2020-08-08 14:15:54.485200+0800 TaggedPointerDemo[55468:2078125] 0xdc527050ee978a35 abcdefgh NSTaggedPointerString
2020-08-08 14:15:54.485644+0800 TaggedPointerDemo[55468:2078125] 0xdcd85e404adcb774 abcdefghi NSTaggedPointerString
2020-08-08 14:15:54.486003+0800 TaggedPointerDemo[55468:2078125] 0x28334c2c0 abcdefghij __NSCFString

上图我们可以看到,当字符串的长度为10个以内时,字符串的类型都是NSTaggedPointerString类型,当超过10个时,字符串的类型才是__NSCFString

打印结果分析:

NSTaggedPointer标志位

static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

上面这个方法我们看到,判断一个对象类型是否为NSTaggedPointerString类型实际上是讲对象的地址与_OBJC_TAG_MASK进行按位与操作,结果在跟_OBJC_TAG_MASK进行对比,我们在看下_OBJC_TAG_MASK的定义:

#if OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#else
#   define _OBJC_TAG_MASK 1UL
#endif

我们都知道一个对象地址为64位二进制,它表明如果64位数据中,最高位是1的话,则表明当前是一个tagged pointer类型。

那么我们在看下上面打印出的地址,所有NSTaggedPointerString地址都是0xd开头,d转换为二进制1110,根据上面的结论,我们看到首位为1表示为NSTaggedPointerString类型。在这里得到验证。

注意:TaggedPointer类型在iOS和MacOS中标志位是不同的iOS为最高位而MacOS为最低位

对象类型

正常情况下一个对象的类型,是通过这个对象的ISA指针来判断的,那么对于NSTaggedPointer类型我们如何通过地址判断对应数据是什么类型的呢?

objc4-723之前

在objc4-723之前,我们可以通过与判断TaggedPointer标志位一样根据地址来判断,而类型的标志位就是对象地址的61-63位,比如对象地址为0xa开头,那么转换成二进制位1010,那么去掉最高位标志位后,剩余为010,即10进制中的2。

接着我们看下runtime源码objc-internal.h中有关于标志位的定义如下:

#if __has_feature(objc_fixed_enum)  ||  __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{// 60-bit payloadsOBJC_TAG_NSAtom            = 0, OBJC_TAG_1                 = 1, OBJC_TAG_NSString          = 2, OBJC_TAG_NSNumber          = 3, OBJC_TAG_NSIndexPath       = 4, OBJC_TAG_NSManagedObjectID = 5, OBJC_TAG_NSDate            = 6,// 60-bit reservedOBJC_TAG_RESERVED_7        = 7, // 52-bit payloadsOBJC_TAG_Photos_1          = 8,OBJC_TAG_Photos_2          = 9,OBJC_TAG_Photos_3          = 10,OBJC_TAG_Photos_4          = 11,OBJC_TAG_XPC_1             = 12,OBJC_TAG_XPC_2             = 13,OBJC_TAG_XPC_3             = 14,OBJC_TAG_XPC_4             = 15,OBJC_TAG_First60BitPayload = 0, OBJC_TAG_Last60BitPayload  = 6, OBJC_TAG_First52BitPayload = 8, OBJC_TAG_Last52BitPayload  = 263, OBJC_TAG_RESERVED_264      = 264
};
#if __has_feature(objc_fixed_enum)  &&  !defined(__cplusplus)
typedef enum objc_tag_index_t objc_tag_index_t;
#endif

那么我们知道2表示的OBJC_TAG_NSString即字符串类型。因为目前已经无法验证这种情况了 所以我们不做其他类型验证。

objc4-750之后
// Returns a pointer to the class's storage in the tagged class arrays.
// Assumes the tag is a valid basic tag.
static Class *
classSlotForBasicTagIndex(objc_tag_index_t tag)
{uintptr_t tagObfuscator = ((objc_debug_taggedpointer_obfuscator>> _OBJC_TAG_INDEX_SHIFT)& _OBJC_TAG_INDEX_MASK);uintptr_t obfuscatedTag = tag ^ tagObfuscator;// Array index in objc_tag_classes includes the tagged bit itself
#if SUPPORT_MSB_TAGGED_POINTERS 高位优先return &objc_tag_classes[0x8 | obfuscatedTag];
#elsereturn &objc_tag_classes[(obfuscatedTag << 1) | 1];
#endif
}

classSlotForBasicTagIndex() 函数的主要功能就是根据指定索引 tag 从数组objc_tag_classes中获取类指针,而下标的计算方法发是根据外部传递的索引tag。比如字符串 tag = 2。当然这并不是简单的从数组中获取某条数据。

 uint16_t NSString_Tag = 2;
uint16_t NSNumber_Tag = 3;
// 3 = 0011
// _OBJC_TAG_INDEX_MASK = 0x7 = 0111uintptr_t string_tagObfuscator = ((objc_debug_taggedpointer_obfuscator>> _OBJC_TAG_INDEX_SHIFT)& _OBJC_TAG_INDEX_MASK);uintptr_t number_tagObfuscator = ((objc_debug_taggedpointer_obfuscator>> _OBJC_TAG_INDEX_SHIFT)& _OBJC_TAG_INDEX_MASK);// 异或操作 相同返回0 不同返回1
// 2 ^ 3 = 0010 ^ 0011 = 0001
// 3^ 3 = 0011 ^ 0011 = 0000uintptr_t string_obfuscatedTag = NSString_Tag ^ string_tagObfuscator;uintptr_t number_obfuscatedTag = NSNumber_Tag ^ number_tagObfuscator;// 按位或
// 1000 | 0001 = 1001 = 9
// 1000 | 0000 = 1000 = 8NSLog(@"%@", objc_tag_classes[0x8 | string_obfuscatedTag]);NSLog(@"%@", objc_tag_classes[0x8 | number_obfuscatedTag]);

控制台输出为:

TaggedPointer[89420:3027642] NSTaggedPointerString
TaggedPointer[89420:3027642] __NSCFNumber

当我们多次运行时,我们发现实际上每次获取到的string_tagObfuscatornumber_obfuscatedTag都不一样,但是每次从objc_tag_classes中取出的类型均是一致的,因此实际上每次运行objc_tag_classes中的内容也是不断变化的。

如果你想进一步的了解可以参考Objective-C中伪指针Tagged Pointer

NSCFNumber

下面我们在看下NSNumber类型

    NSNumber *number1 = @(0x1);NSNumber *number2 = @(0x20);NSNumber *number3 = @(0x3F);NSNumber *numberFFFF = @(0xFFFFFFFFFFEFE);NSNumber *maxNum = @(MAXFLOAT);NSLog(@"number1 pointer is %p class is %@", number1, number1.class);NSLog(@"number2 pointer is %p class is %@", number2, number2.class);NSLog(@"number3 pointer is %p class is %@", number3, number3.class);NSLog(@"numberffff pointer is %p class is %@", numberFFFF, numberFFFF.class);NSLog(@"maxNum pointer is %p class is %@", maxNum, maxNum.class);

我们在看下打印结果:

TaggedPointerDemo[59218:2167895] number1 pointer is 0xf7cb914ffb51479a class is __NSCFNumber
TaggedPointerDemo[59218:2167895] number2 pointer is 0xf7cb914ffb51458a class is __NSCFNumber
TaggedPointerDemo[59218:2167895] number3 pointer is 0xf7cb914ffb51447a class is __NSCFNumber
TaggedPointerDemo[59218:2167895] numberffff pointer is 0xf7346eb004aea86b class is __NSCFNumber
TaggedPointerDemo[59218:2167895] maxNum pointer is 0x28172a0c0 class is __NSCFNumber

我们发现对于NSNumber,我们打印出来的数据类型均为__NSCFNumber,但是我们发现对于MAXFLOAT打印出的地址显然与其他几项不符,上面几个NSNumber的地址以0xf开头,根据字符串地址的经验我们可以看出f = 1111,首位标记位为1,表示这个数据类型属于TaggedPointer。而MAXFLOAT不是。

获取TaggedPointer的值

objc4-723之前

字符串:

从上图的地址中我们就可以看出,从低位到高位分别表示的就是字符串的值(在ASCII码表中的值)

数字:

对于数字来说从地址中也是直接读出存储的值,如上图。

objc4-750之后
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}static inline uintptr_t
_objc_getTaggedPointerValue(const void * _Nullable ptr)
{// assert(_objc_isTaggedPointer(ptr));uintptr_t value = _objc_decodeTaggedPointer(ptr);uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;if (basicTag == _OBJC_TAG_INDEX_MASK) {return (value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;} else {return (value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;}
}static inline intptr_t
_objc_getTaggedPointerSignedValue(const void * _Nullable ptr)
{// assert(_objc_isTaggedPointer(ptr));uintptr_t value = _objc_decodeTaggedPointer(ptr);uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;if (basicTag == _OBJC_TAG_INDEX_MASK) {return ((intptr_t)value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;} else {return ((intptr_t)value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;}
}

示例代码:

  NSString *str1 = [NSString stringWithFormat:@"1"];NSString *str11 = [NSString stringWithFormat:@"11"];NSString *str2 = [NSString stringWithFormat:@"2"];NSString *str22 = [NSString stringWithFormat:@"22"];// 0x31 1 0x32 1uintptr_t value1 = objc_getTaggedPointerValue((__bridge void *)str1);uintptr_t value2 = objc_getTaggedPointerValue((__bridge void *)str2);uintptr_t value11 = objc_getTaggedPointerValue((__bridge void *)str11);uintptr_t value22 = objc_getTaggedPointerValue((__bridge void *)str22);// 以16进制形式输出NSLog(@"%lx", value1);NSLog(@"%lx", value11);NSLog(@"%lx", value2);NSLog(@"%lx", value22);

控制台输出:

TaggedPointer[89535:3033433] 311
TaggedPointer[89535:3033433] 31312
TaggedPointer[89535:3033433] 321
TaggedPointer[89535:3033433] 32322

即 “1” = 0x31 1,最后一位表示长度,在ASCII码表中31表示的就是字符1。而且从字符串“11”的结果我们也可以验证上面的说法。

isa 指针(NONPOINTER_ISA)

上面我们说了,对于一个对象的存储,苹果做了优化,那么对于ISA指针呢?

对象的isa指针,用来表明对象所属的类类型。

union isa_t {isa_t() { }isa_t(uintptr_t value) : bits(value) { }Class cls;uintptr_t bits;
#if defined(ISA_BITFIELD)struct {ISA_BITFIELD;  // defined in isa.h};
#endif
};

同时结合下图,我们可以更清晰的了解isa指针的作用以及类对象的概念。

从图中可以看出,我们所谓的isa指针,最后实际上落脚于isa_t的联合类型。那么何为联合类型呢?
联合类型是C语言中的一种类型,是一种n选1的关系,联合的作用在于,用更少的空间,表示了更多的可能的类型,虽然这些类型是不能够共存的。比如isa_t 中包含有clsbitsstruct三个变量,它们的内存空间是重叠的。在实际使用时,仅能够使用它们中的一种,你把它当做cls,就不能当bits访问,你把它当bits,就不能用cls来访问。

对于isa_t联合类型,主要包含了两个构造函数isa_t(),isa_t(uintptr_t value)和三个变量cls,bits,struct,而uintptr_t的定义为typedef unsigned long

当isa_t作为Class cls使用时,这符合了我们之前一贯的认知:isa是一个指向对象所属Class类型的指针。然而,仅让一个64位的指针表示一个类型,显然不划算。

因此,绝大多数情况下,苹果采用了优化的isa策略,即,isa_t类型并不等同而Class cls, 而是struct

struct

下面我们先来看下struct的结构体

// ISA_BITFIELD定义如下
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   define ISA_BITFIELD                                                      \uintptr_t nonpointer        : 1;                                       \uintptr_t has_assoc         : 1;                                       \uintptr_t has_cxx_dtor      : 1;                                       \uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \uintptr_t magic             : 6;                                       \uintptr_t weakly_referenced : 1;                                       \uintptr_t deallocating      : 1;                                       \uintptr_t has_sidetable_rc  : 1;                                       \uintptr_t extra_rc          : 19
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

注意:成员后面的:表明了该成员占用几个bit
而每个成员的意义如下表

标志位说明

成员 bit位 说明
nonpointer 1bit 标志位。1(奇数)表示开启了isa优化,0(偶数)表示没有启用isa优化。所以,我们可以通过判断isa是否为奇数来判断对象是否启用了isa优化
has_assoc 1bit 标志位。表明对象是否有关联对象。没有关联对象的对象释放的更快。
has_cxx_dtor 1bit 标志位。表明对象是否有C++或ARC析构函数。没有析构函数的对象释放的更快
shiftcls 33bit 类指针的非零位。
magic 6bit 固定为0x1a,用于在调试时区分对象是否已经初始化。
weakly_referenced 1bit 标志位。用于表示该对象是否被别的对象弱引用。没有被弱引用的对象释放的更快。
deallocating 1bit 标志位。用于表示该对象是否正在被释放。
has_sidetable_rc 1bit 标志位。用于标识是否当前的引用计数过大,无法在isa中存储,而需要借用sidetable来存储。(这种情况大多不会发生)
extra_rc 19bit 对象的引用计数减1。比如,一个object对象的引用计数为7,则此时extra_rc的值为6。

从上表我们发现,extra_rchas_sidetable_rc是和引用计数相关的标志位,当extra_rc 不够用时,还会借助sidetable来存储计数值,这时,has_sidetable_rc会被标志为1。

接下来我们来验证下,这些标志位是否真的如表中介绍那样。

引用计数

我们先来看下面这段代码

- (void)testisa {NSObject *obj = [[NSObject alloc] init];NSLog(@"1. obj isa_t = %p", *(void **)(__bridge void*)obj);
}

控制台输出结果

TaggedPointerDemo[59983:2185591] 1. obj isa_t = 0x1a1f335beb1

我们将地址0x1a1f335beb1转换过后:

我们看到这时候 对象是nonpointer开启了isa优化,且当前的引用计数器为 extra_rc = 0 + 1 = 1;

下面我们接着测试

    NSObject *obj = [[NSObject alloc] init];NSLog(@"1. obj isa_t = %p", *(void **)(__bridge void*)obj);_obj1 = obj;NSObject *tmpObj = obj;NSLog(@"2. obj isa_t = %p", *(void **)(__bridge void*)obj);

控制台输出为

TaggedPointerDemo[63235:2266690] 1. obj isa_t = 0x1a1f335beb1
TaggedPointerDemo[63235:2266690] 2. obj isa_t = 0x41a1f335beb1

我们将地址0x41a1f335beb1转换过后:

我们看到这时候,我们将obj强引用之后,又实用了一个局部变量对其进行引用,所以这时的引用计数应该为2,当然从图中我们也可以验证这一点。

weakly_referenced

我们这次添加一个弱引用来验证

 _weakRefObj = _obj1;
NSLog(@"3. obj isa_t = %p", *(void **)(__bridge void*)_obj1);

控制台输出为

TaggedPointerDemo[63235:2266690] 3. obj isa_t = 0x45a1f335beb1

这时候我们仅仅通过地址进行判断 当添加了_obj2 = _obj1后,地址变为0x61a1f335beb1与之前地址0x41a1f335beb1对比

上图我们可以看到weakly_referenced标志位被置为1.表示这个对象有被弱引用。

has_assoc

然后我们在添加一个关联属性

NSObject *attachObj = [[NSObject alloc] init];
objc_setAssociatedObject(_obj1, "attachKey", attachObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSLog(@"4. obj isa_t = %p", *(void **)(__bridge void*)_obj1);

控制台输出为:

TaggedPointerDemo[63235:2266690] 4. obj isa_t = 0x45a1f335beb3

从上图中我们看到has_assoc标志位被置为1.

总结

截止到这里,我们通过观察NSTaggedPointer,相关标志位我们基本了解了NSTaggedPointer是如何存储数据以及标志位的作用。

参考文章

Objective-C中伪指针Tagged Pointer

Friday Q&A 2015-07-31: Tagged Pointer Strings

内存管理之Tagged pointer相关推荐

  1. 内存管理-定时器循环、内存布局、tagged pointer、weak指针、copy、自动释放池

    先上代码,我们平时用的定时器,cadisplaylink.nstimer,CADisplayLink.NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用 ...

  2. Objective-C runtime机制(5)——iOS 内存管理

    概述 当我们创建一个对象时: SWHunter *hunter = [[SWHunter alloc] init]; 上面这行代码在栈上创建了hunter指针,并在堆上创建了一个SWHunter对象. ...

  3. Tagged Pointer分析

    在objc4源码中,我们经常会在函数中看到Tagged Pointer.Tagged Pointer究竟是何方神圣? 从64位系统开始,iOS引入了Tagged Pointer技术,用于优化小对象(N ...

  4. macOS 与 iOS 中的 Tagged Pointer

    目录 Tagged Pointer 的简介 解除 Tagged Pointer 的数据混淆 Tagged Pointer 的原理:macOS Tagged Pointer 的原理: iOS Tagge ...

  5. iOS之深入解析内存管理Tagged Pointer的底层原理

    一.前言 ① Tagged Pointer 概念 iOS 开发者对"引用计数"这个名词肯定不陌生,引用计数是苹果为了方便开发者管理内存而引入的一个概念.当引用计数为 0 时,对象就 ...

  6. (0019)iOS 开发之关于__weak修饰NSString以及内存管理的问题

    前言:写这篇文章的初衷,是对阅读别人的博客提出的疑问,一路探索得来的.同时也要加强对内存管理以及block 的管理和使用. ARC指南1 - strong和weak指针 写到:打印出来是"( ...

  7. multiprocessing.manager管理的对象需要加锁吗_iOS内存管理布局-理论篇

    苹果设备备受欢迎的背后离不开iOS优秀的内存管理机制,那iOS的内存布局及管理方案是怎样的呢?我们一起研究下. 内存管理分为五大块 栈区(stack):线性结构,内存连续,系统自己管理内存,程序运行记 ...

  8. multiprocessing.manager管理的对象需要加锁吗_iOS内存管理布局及管理方案理论篇

    苹果设备备受欢迎的背后离不开iOS优秀的内存管理机制,那iOS的内存布局及管理方案是怎样的呢?我们一起研究下. 内存管理分为五大块 栈区(stack):线性结构,内存连续,系统自己管理内存,程序运行记 ...

  9. iOS - 内存管理

    一.App 内存分布 二.OC对象的内存管理 iOS 中,使用引用计数来管理 OC 对象的内存,新创建的 OC 对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间.调用 r ...

最新文章

  1. .NET Core 跨平台 串口通讯 ,Windows/Linux 串口通讯
  2. php接口返回一个数组怎末写_php api返回json数组
  3. 【UML关系(泛化、实现、依赖、关联(聚合,组合))】
  4. XAF 如何使用复合主键和复合外键
  5. 给妹子讲python-S01E01好用的列表
  6. 大数据时代,如何做商业智能产品选型
  7. A53系统移植、内核、文件系统
  8. 对SendMessage与PostMessage的理解
  9. php订单管理系统(源码+数据库+截图)
  10. ASCII码与16进制的互相转换(表)
  11. 基于飞桨 DeepLabV3+实现人体肾组织图像中肾小球识别
  12. 动态内存的申请和非动态内存的申请_深圳罗湖“限制非深户申请公办学位”惹争议,官方权威回应来了...
  13. Origin 2017调整画布和图表的尺寸大小
  14. 关系型数据库由哪三部分组成_关系数据库| 第2部分
  15. 我迄今见过最完美的中文编程开发工具
  16. Benchmark分析[4]: parsec
  17. android 自定义圆形裁剪框,android 头像裁剪控件
  18. Acwing2058. 笨拙的手指
  19. plant simulation物流系统仿真案例
  20. 全栈Python 编程必备

热门文章

  1. JAVAWEB学习笔记--Day3
  2. ios 单元测试覆盖率怎么查看_iOS 覆盖率检测原理与增量代码测试覆盖率工具实现...
  3. java 大小写匹配_大写字母的Java正则表达式
  4. 开源=安全?RVN盗币事件复盘
  5. 计算机科学与探索支付宝,中国第三方网上支付发展研究-以支付宝为例(毕业论文).doc...
  6. vue2.0安装3.0安装,配置介绍功能模块
  7. win7如何显示文件后缀名【系统天地】
  8. 关于自制CMSIS_DAP离线下载器下载算法的代码说明:“0xE00ABE00, 0x062D780D, 0x24084068, 0xD3000040, 0x1E644058, 0x1C49D1FA“
  9. 从G1到冻酸奶Froyo
  10. zookeeper读书笔记十 zookeeper实现分布式屏障Barriers