由 NSObject *obj = [[NSObject alloc] init] 引发的一二事儿

栏目: IOS · 发布时间: 5年前

内容简介:接下来跟大家分享一下 Objective-C 和 iOS 开发的基础内容,而且主要会围绕一句普通的代码进行展开:其实这部分内容大都是我自己对这行代码冒出的一些的问题和想法进行的解释,而且准备得有些仓促,所以难免会有些不全面和错漏的地方,请多多见谅~我们先来看看这句代码的基本含义,尝试从 NSObject 这个角度去解读

接下来跟大家分享一下 Objective-C 和 iOS 开发的基础内容,而且主要会围绕一句普通的代码进行展开:

NSObject *obj = [[NSObject alloc] init];
复制代码

其实这部分内容大都是我自己对这行代码冒出的一些的问题和想法进行的解释,而且准备得有些仓促,所以难免会有些不全面和错漏的地方,请多多见谅~

基本含义

我们先来看看这句代码的基本含义,尝试从 NSObject 这个角度去解读

这行代码中写有两个 NSObject ,但他们表示的意思是不一样的。

等号左边表示:创建了一个 NSObject 类型的指针 obj 。(开辟一个 NSObject 类型大小的内存空间,并用指针变量 obj 指向它) 等号右边表示:调用 NSObject 对象的类方法 alloc 进行内存空间的分配,调用实例方法 init 进行构造工作,如成员变量的初始化等。 等号右边的 NSObject 对象初始化完成之后将内存地址赋值给左边的 obj 。

new 方法和 alloc/init

感觉使用 Java 的人经常会说 new 一个对象,虽然 Objective-C 也给我们提供了这个方法,但我们却很少直接使用 new ,而是使用 alloc init ,为什么?

使用 new 和 使用 alloc init 都可以创建一个对象,而且在 new 的内部其实也是调用 alloc 和 init 方法,只是 alloc 会在分配内存时使用到 zone ,其实总体来看没啥区别。

NSZone 是 Apple 用来处理内存碎片化的优化方式,处理对象的初始化及释放等问题,以提高性能。但据说效果并不好。

使用 new 的好处是什么?

  • 简单,比使用 alloc init 少码一个单词和一对中括号

使用 alloc init 的好处是什么?

  • 显式调用, 少了 new 内部实现的转换,可能速度更快 (速度慢)
  • 支持自定义构造方法。一般在我们的自定义类的构造方法中都会使用到如 initWithXxx

为啥 Objective-C 的构造方法都是 initWithXxx ,而 swift 可以使用 init

initWithXxx

只是声明一个变量 NSObject *obj; ?swift 呢 ?

在 Objective-C 中允许只声明一个变量并被使用,编译器不会报错。如果声明的是一个 Objective-C 对象,输出的值是 null ,如果是基本类型,输出的值是 0 ,如果是结构体如 CGRect ,会用 0 填充。

但是在 swift 中,情况就不一样了,声明一个变量如 let a : Int 并被使用时,编译器会对这种行为报以错误提示, Variable 'a' used before being initialized ,表示变量 a 在使用前未被初始化。

为什么?

其实在 Objective-C 中声明一个变量时,它是会有一个默认值的,但在 swift 中则不会提供默认值,因为 swift 作为一种强类型语言,它总是强制类型定义,并且要求变量的使用要严格符合定义,所有变量都必须先定义,且初始化后才能被使用。

这里有一个例外 -- 可选类型,如 let b : Int? ,可选属性不需要设置初始值,默认的初始值都是 nil ,不管是基础类型还是对象类型的可选类型属性的初始值都是 nil 。而且可选类型是在设置数值的时候才分配空间,是一种 lazy-evaluation 即延迟计算的行为。

Objective-C 和 swift 构造方法顺序的区别?

  • 在 swift 的构造方法中,先正确初始化本类的成员变量,再调用父类的构造方法
  • 在 Objective-C 的构造方法中,先调用父类的构造方法,再正确初始化本类的成员变量

构造方法 init 中使用 if(self = [super init]) 写法

平常在构造方法里做一些初始化工作时都会写上这样的代码,而 self = [super init] 这里先调用父类的构造方法也符合上述的构造顺序问题,但为什么需要 if 来保证 self 被正确初始化?难道 self = [super init] 不安全?

self = [super init] 确实不安全,因为 self 真的可能没有被正确初始化。

我们看回最开始的那行代码 NSObject *obj = [[NSObject alloc] init]; ,这里会包括三个步骤:

  1. alloc 分配内存
  2. init 在内存的位置上调用构造方法
  3. 将内存地址赋值给 obj

这一切好像并没有任何问题,但由于 CPU 的乱序执行能力,使得步骤 2 和步骤 3 的执行顺序可能发生颠倒,也就是说,完全有可能出现这样的情况:obj 的值已经不是 null (已调用步骤 3),但对象仍然没有构造完成(未调用步骤 2),这时候如果出现一些并发调用的情况使用了未正确初始化的对象 obj ,那么很可能就会出现不可预料的后果,甚至发生 crash 。

所以这里使用 if 是很有必要的,以保证得到的 self 已经被正确的初始化。

Objective-C 中的基础类型和对象类型

在这行代码 NSObject *obj = [[NSObject alloc] init]; 等号左边的 NSObject 表示的是对象类型,那么在 Objective-C 中常见的基础类型和对象类型有哪些

基础类型:

  • 整型:int(32位)、Integer(根据计算机位数调整)
  • 浮点型:float(4 字节)、double(8 字节)、CGFloat(根据计算机位数调整)
  • 字符型:1 字节,Objective-C 字符变量不支持中文字符,字符需要使用 ` ` 包起来,char 类型也可以看作整型值来使用,它是一个 8 位无符号整数
  • 布尔型:YES、NO
  • 枚举型

对象类型:

  • NSObject
  • NSString 及可变版本 NSMutableString
  • NSArray 及可变版本 NSMutableArray
  • NSDictionary 及可变版本 NSMutableDictionary

这里有一个注意点是,有可变与不可变类型的对象,为了安全起见,用 copy 修饰不可变版本,用 strong 修饰可变版本,这样做的原因是,如果有一个不可变的字符串 str 且用 strong 修饰,这时被赋值了一个可变字符串 mStr ,这样可能会发生这样的情况:一个本来预想中不可变的字符串 str 会因 mStr 的改变而改变。所以这里要仔细考量一下使用 copy 还是 strong 去修饰。

Objective-C 世界中的 “非 0 即真” ?

在 Objective-C 中,BOOl 的定义是这样的:

typedef signed char BOOL;
#define YES (BOOL)1
#define NO  (BOOL)0
复制代码

其他相关的布尔型如下:

bool :

C99标准定义了一个新的关键字_Bool,提供了布尔类型
#define bool _Bool
#define true 1    
#define false 0
复制代码

Boolean:

typedef unsigned char Boolean;
enum DYLD_BOOL { FALSE, TRUE };
复制代码

Objective-C 对象存储在堆区

上面谈到了 NSObject 作为类型展开的一些内容,现在我们来看看 NSObject 作为对象来延伸出 Objective-C 对象存储位置相关的内容。

先来看看内存的五大区:

  • 静态区(BSS 段)
  • 常量区(数据段)
  • 代码段

还是回到最开始的那行代码来进行解释:

NSObject *obj = [[NSObject alloc] init];
复制代码

我们知道,在 Objective-C 中,对象通常是指一块有 特定布局 的连续内存区域。这行代码创建了一个 NSObject 类型的指针 obj 和一个 NSObject 类型的对象,obj 指针存储在栈上,而其指向的对象则存储在堆上。

在栈上就不能创建对象吗?

* 不能直接创建,但可通过在结构体中的 isa 来间接创建对象。

  • 其实 block 也可以存储在栈上

那么这里又带来了几个问题,isa 和 block ,这在后面会单独聊。

栈对象的优缺点

那么为什么 Objective-C 会选择使用堆来存储对象而不是栈,来看看栈对象的优缺点。

优点:

  • 创建速度和运行时速度快:相对于堆对象创建时间快几十倍;编译期能确定大部分内存布局,因而在运行时分配空间几乎不耗时
  • 生命周期固定:对象出栈就会被释放,不会存在内存泄漏

缺点:

  • 生命周期固定,可能会出现这种情况:一个栈对象被创建之后被传递到别的方法,当栈对象的创建方法返回时,栈对象会被一起 pop 出栈而释放,导致没法在别处被继续持有,此时 retain 会失效,因此,栈对象会给对象的内存管理造成相当大的麻烦。
  • 空间:栈跟线程具有绑定关系,而栈的可用空间非常有限的。因此对象如果都在栈上创建不太现实,而堆只要物理内存不警告即可使用。
  • 512 KB (secondary threads)
  • 8 MB (OS X main thread)
  • 1 MB (iOS main thread)

综上,Objective-C 选择使用堆存储对象。

NSString 的存储位置

关于 NSString 的存储位置非常复杂,可以分配在栈区、堆区、常量区,粗略的理解如下:

  • 当创建的 NSString 类型底层是 NSTaggedPointerString 时,其本质不再是一个对象,而是真正的值,存储在栈区
  • 当创建的 NSString 类型底层是 __NSCFConstantString 时,其 retainCount 极大,意味着不会被释放,存储在常量区(一般通过字面量进行创建)

block 的存储位置

block 可以存储在栈上,也可以存储在堆上。通常我们会使用 copy 将一个栈上的 block 复制到堆上。

顺便谈谈关于 block 的其他内容:

block 的意思是拥有自动变量的匿名函数。

  • 在 Objective-C 中称为 block
  • 在 swift 中称为闭包
  • 在 Java 中称为 lambda (也称闭包)

这里要注意的是,由于栈对象的有效区域仅限于其所在的块 {} ,即其捕获自动变量的范围也仅限于所在块。

还有一个修改自动变量时的注意点是:

  • 静态全局变量,全局变量由于作用域的原因,可以直接在 block 里被修改
  • 静态变量由于传递给 block 的是内存地址值,所以也能在 block 里被修改
  • 默认情况下是不允许修改捕获到的自动变量值,但我们可以通过使用 __block(storage-class-specifier,存储域说明符) 修饰变量来使该变量也能在 block 里被修改

Mansory 的 block 为什么不循环引用

到这儿由 block 联想到了我们项目中使用到的自动布局框架 Mansory ,比如项目中的一个代码片段:

[self.carousel mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.top.mas_equalTo(self.headerView);
            make.left.mas_equalTo(self.headerView).offset(20);
            make.right.mas_equalTo(self.headerView).offset(-20);
            make.height.mas_equalTo(CGFLOAT_MIN);
        }];
复制代码

通常在使用 block 时都会避免在 block 内部使用 self ,以免产生循环引用,造成内存泄漏,所以通常会在 block 外部对 self 进行一次弱引用,再在内部进行一次强引用,用这种组合做法来避免产生循环引用现象,这里的循环引用现象可能是:self -> block -> self 。

然后我们通过观察其源码实现来进一步了解:

- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    constraintMaker.removeExisting = YES;
    block(constraintMaker);
    return [constraintMaker install];
}
复制代码

结合前面介绍的关于 block 存储位置的内容,我们可以知道,虽然 block 内部引用了 self ,但由于这是一个局部的 block ,存储在栈上而不是堆上,因而在出了 block 所在作用域后会被 pop 出栈而自动销毁,所以不存在引用环。

聊聊 block 和 Delegate

讲完 block 的存储位置,自然会想到它的一些使用场景,特别是选在择使用 block 还是代理的一些争执吧。

其实我觉得如何进行选择更多的是依据个人的一些编码风格和习惯,还有就是要符合原有项目的需要,说到底使用 block 和代理都没问题。但由于一些函数式框架的出现,比如 RAC 、RxSwift 、 promisekit ,里面链式调用 + 闭包的操作实在是很方便,而且也更加符合低耦合高内聚的编程理念,所以可能选择使用 block 又多了一个理由。

下面再聊一下 block 和 delegate 的一些本质区别,这部分内容主要是引述微信技术群里面的一位大佬的解释:

代理的 debug 追踪性确实会比 block 好,但是如果跟 block 在可读性方面比较的话其实算是弱项。

代理和 block 实际上都是函数 imp 的调用,但区别是,代理就等价于 weak 持有一个代理对象,你不写 protocol 不写 delegate ,一股脑把所有方法全写在 header 里,然后把代理对象本身直接传过去给另一个对象,在另一个对象中 weak 持有这个代理对象,这种写法和代理是没有区别的。

而 block 是一种还原上下文环境,甚至自动包裹一些自由变量的闭包概念,换句话说,block 的回调代码,和写 block 的代码,是可以同处于一个函数内,在一个可读代码上下文内,即 block 在代码上是一个连续的过程。

代理方法实际上传值传的是一整个对象,你把 a.delegate = self 其实是把 self 传给了 a 持有,跟一般的属性赋值无异,如果再次传递,完全可以继续传递 self 给别人。

block 继续传递,实际上是把 imp 和上下文环境的自动变量打包进行传递,这个过程中不一定会传递一个对象。从这个角度看 block 的控制力度更强一些。

这里会涉及到一个安全性方面的考虑,你把 self 传给了一个不知名的三方库,他虽然只是 id 看起来只能调用 protocol 里限定的方法,但其实 OC 这个约束只是骗骗编译器的。如果你把一个 self 传给了一个三方,设定为代理,如果三方有其他意图,他其实可以直接控制你的 self 对象的任意值或者方法。但 block ,你传过去的 block ,他只能操作 block 本身包裹的上下文环境。

ARC 下编译器自动补 __strong 修饰

扯得有点远了,咱回头看回最开始的那行代码 NSObject *obj = [[NSObject alloc] init]; ,在 ARC 下会变成 __strong NSObject *obj = [[NSObject alloc] init]; ,这涉及到 iOS 开发的内存管理相关内容。

在早期 macOS 开发中使用 GC 进行内存管理但现在都跟 iOS 开发一样已统一使用引用计数进行内存管理:当对象的引用计数为 0 时会被销毁,当对象被引用时其引用计数会 +1 ,当对象的引用被销毁时引用计数 -1 。

  • ARC(automatic reference counting),自动引用计数
  • MRC(manual reference counting),手动引用计数

__strong 是一个变量修饰符,但这里不打算列举其他的变量修饰符,而会在下一条聊聊关于内存管理相关的属性修饰符。

属性修饰符

内存管理相关的变量修饰符都有相对应的属性修饰符,一般的写法是在属性修饰符前添加两个下划线。

这里列举一下内管管理语义的属性修饰符:

  • assign
  • strong
  • weak
  • unsafe_unretained
  • copy

设置上述属性修饰符会在属性自动生成 setter 方法的时候为我们添加内存管理语义,明确内存管理所有权,如果我们自定义 setter 访问器,则需手动指定。

  • assign:在 setter 方法中只是进行简单的赋值操作。适用于一些标量类型和 id 类型,如 CGFloat、NSInteger 。
  • strong:会指定所有权关系。在 setter 方法中,当一个新值要被设置时,首先 retain 新值,然后 release 旧值,最后再进行赋值。
  • weak:会指定无所有权关系。在 setter 方法中,当一个新值要被设置时,既不会 retain 新值,也不会 release 旧值,只会进行简单的赋值操作。在属性所指向的对象销毁时,属性值会清空。
  • unsafe_unretained:跟 assign 的语义相同,但适用于 OC 对象。并且与 weak 不同的是,在属性所指向的对象销毁时,属性值不会被清空。
  • copy:与 strong 类似,但在 setter 方法中不会 retain 新值,而是将其拷贝。

标量类型缺省是 assign ,对象类型缺省是 strong 。

再聊聊属性 @property

我们知道,属性 = 实例变量(ivar) + setter + getter ,他的具体过程是这样的:

完成属性定义后,编译器会自动编写访问这些属性所需的访问器,此过程称为“自动合成”(autosynthesize)。需要强调的是,这个过程由编译器在编译期执行,所以在编译器中看不到自动生成的源代码。除了生成访问器 getter 、setter 之外,编译器还要自动向类中添加适当类型的实例变量,实例变量名称是在属性名前加下划线,我们也可以在类的实现代码里通过 @synthsize 语法来指定实例变量的名字,如:

@implementation Person
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
复制代码

这里有一个注意的地方, @synthesize firstName; 像这样不指定实例变量的名字,那么生成的实例变量名会跟属性名一致,而不会再加下划线。

还有一个要注意的关键字:@dynamic ,他不会在编译阶段自动生成 getter 和 setter 方法,而且使用点语法或者赋值操作在编译阶段仍能够通过,但是该属性的访问器必须在运行时由用户自己实现,否则会 crash 。

isa

在前面我们有说到 Objective-C 对象通常是一块有特定布局的连续内存区域,所以接下来牵扯的内容可能会扯得比较远。

在计算机网络中有一堆协议,遵守同一个协议,那么他们之间便可以知晓对方的身份,接着愉快的进行通信。

那么 Objective-C 作为一门面向对象语言,他是怎样判断一个东西是不是对象,又是如何进行对象间的通信?

Objective-C 中的对象是一个指向 ClassObject 地址的变量: id obj = &ClassObject

这个地址其实就是在最高位的 isa 指针。

而对象的实例变量则是:

void *ivar = isa + offset(N)

所以 isa 就相当于对象之间的一个协议。

Objective-C 面向对象的一个 bug :比如一个 Person 实例,她调用一个 talk 方法,按理说,这个 talk 方法应该直接在该实例里面调用对应的实现,但是实际却不是这样,她会通过实例自己的 isa 指针找到对应的类,然后在类或及其类的继承结构一直往上寻找 talk 方法,该方法被找到后就会调用,这样看来就并不是最初的那个实例进行调用。

atomic 不安全

属性还有一类原子性修饰符,atomic 和 nonatomic ,原子性和非原子性,缺省是原子性的,但在 iOS 开发中,几乎所有属性都会主动声明为 nonatomic 。

原因有两个:

  1. atomic 有性能消耗
  2. atomic 无法保证更大粒度时的安全问题

第一点,是因为使用 atomic 修饰的属性由编译器所合成的方法会通过锁机制(底层使用自旋锁)来确保原子性。 第二点,是因为即便原子操作阻止了属性被多个线程同时进行访问,但这并不代表我们最终使用它们的代码时时线程安全的,比如并发访问粒度更大的实例中的属性,举个例子:

// Person.h
@property(atomic, copy) NSString *firstName;
@property(atomic, copy) NSString *lastName;
- (void)updateWithFirstName:(NSString *)firstName lastName:(NSString *)lastName delay:(double)t on:(dispatch_queue_t) q;

// Person.m
- (void)updateWithFirstName:(NSString *)firstName lastName:(NSString *)lastName delay:(double)t on:(dispatch_queue_t)q{
    if (firstName != nil) {
        self.firstName = firstName;
    }

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(t * NSEC_PER_SEC)), q, ^{
        if (lastName != nil) {
            self.lastName = lastName;
        }
    });

}
    
// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];

    Person *p = [[Person alloc] init];
    p.firstName = @"Holy";
    p.lastName = @"H";

    dispatch_queue_t queueA = dispatch_queue_create("queueA", 0);
    dispatch_queue_t queueB = dispatch_queue_create("queueB", 0);

    dispatch_async(queueA, ^{
        [p updateWithFirstName:@"John" lastName:@"J" delay:0.0 on:queueA];
    });

    dispatch_async(queueB, ^{
        [p updateWithFirstName:@"Ben" lastName:@"B" delay:0.0 on:queueB];
    });

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@ %@", p.firstName, p.lastName);
    });

}
复制代码

现在运行结果可能会出现

Ben J
复制代码

这显然不是我们最初想要得到的。

到目前为止,Apple 公司的开发者们并没有为 swift 的属性提供标记 atomic/nonatomic 的方法,也没有下面提到的 @synchronized 块那样去做互斥操作,而我们可以通过使用 @synchronized 底层使用到的 objc_sync_enter(obj)objc_sync_exit(obj) 去实现,但因为 objc_sync_xxx 是相当底层的方案,一般不推荐直接使用,而应选择其他高阶的方案。

并发

那么如何解决上述示例的问题?我们可以通过在修改时添加 @synchroized(obj) {} 块,将原子操作的粒度扩大到 obj 对象的修改域。

还有其他一些常见的同步机制如:NSLock、pthread、OSSpinLock、信号量等。

iOS 基础

事件产生、传递和响应链

最后简单介绍一下 iOS 开发中事件的产生、传递和响应链。

事件产生:

系统注册了一个 Source 1(基于 mach port)用来接收系统事件,其回调函数为 _IOHIDEventSystemClientQueueCallback() 。当一个硬件事件(比如触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收,SpringBoard 只接收按键(锁屏/静音等)、触摸、加速、传感器等几种 event ,随后用 mach port 转发给需要的 APP 进程。随后苹果注册的哪个 Source 1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会以先进先出的顺序把 IOHIDEvent 处理并包装成 UIEvent 进行处理分发,其中包括识别 UIGesture /处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegan/Moved/End/Cancel 等都是在这个回调中完成的。

触摸事件传递,大致是从父控件传递到子控件:

UIApplication -> UIWindow -> UIView (or Gesture recognizer 这时会被当前 vc 截断) -> 寻找处理事件最合适的 view

那么如何寻找处理事件最合适的 view ?步骤:

  1. 首先判断主窗口能否接收事件
  2. 触摸点是否在自己身上
  3. 若在,从后往前遍历自己的子控件,重复前两个步骤(能否接收事件?是否在控件上?)
  4. 直到找不到合适的子控件,那么自己就成为最合适的 view ,之后调用具体的如 touches 方法处理

底层实现主要涉及两个方法: func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?func point(inside point: CGPoint, with event: UIEvent?) -> Bool

hitTest 方法会根据视图层级结构往上调用 pointInside 方法,确定能否接收事件。如果 pointInside 返回 true ,则继续调用子视图层级结构,直到在最远的视图找到点击的 point 。如果一个视图没有找到该 point ,则不会继续它往上的视图层级结构。

我们可以通过调用这个方法来截获和转发事件。

事件响应,大致是从子控件传递到父控件:

过程:

  1. 如果当前 view 是控制器的 view ,那么控制器就是上一个响应者,事件就传递给控制器;如果当前 view 不是控制器的 view ,那么父视图就是当前 view 的上一个响应者,事件就传递给它的父视图
  2. 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给 window 对象进行处理
  3. 如果 window 对象也不处理,则其将事件或消息传递给 UIApplication 对象
  4. 如果 UIApplication 也不能处理该事件或消息,则将其丢弃

在上述过程中,如果某个控件实现了 touchesXxx 方法,则这个事件将由该控件接管,如果调用 super 的 touchesXxx ,就会将事件顺着响应者链继续往上传递,接着会调用上一个响应者的 touchesXxx 方法。

一般我们会选择使用 block 或者 delegate 或者 notification center 去做一些消息事件的传递,而现在我们也可以利用响应者链的关系来进行消息事件的传递。


以上所述就是小编给大家介绍的《由 NSObject *obj = [[NSObject alloc] init] 引发的一二事儿》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

可爱的Python

可爱的Python

哲思社区 / 电子工业出版社 / 2009-9 / 55.00元

本书的内容主要来自CPyUG社区的邮件列表,由Python的行者根据自身经验组织而成,是为从来没有听说过Python的其他语言程序员准备的一份实用的导学性质的书。笔者试图将优化后的学习体验,通过故事的方式传达给读者,同时也分享了蟒样(Pythonic式)的知识获取技巧,而且希望将最常用的代码和思路,通过作弊条(Cheat Sheet,提示表单)的形式分享给有初步基础的Python 用户,来帮助大家......一起来看看 《可爱的Python》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具