【原】iOS开发进阶(唐巧)读书笔记(二)
<h3 id="第三部分ios开发底层原理">第三部分:iOS开发底层原理</h3><h4 id="1objective-c对象模型">1、Objective-C对象模型</h4>
<h4 id="11-isa指针">1.1 isa指针</h4>
<p><strong>NSObject.h部分代码:</strong></p>
<pre><code>NS_ROOT_CLASS
@interface NSObject <NSObject> {
Class isa;
}
</code></pre>
<p><strong>objc.h部分代码:</strong></p>
<pre><code>typedef struct objc_class *Class;
typedet struct objc_object {
Class isa;
} *id;
</code></pre>
<blockquote>
<p>每个对象都有一个名为isa的指针,指向该对象的类</p>
</blockquote>
<p><strong>isa指针指向流程图如下:</strong></p>
<p><img src="https://img2018.cnblogs.com/blog/793084/201910/793084-20191025162700936-49998528.png"></p>
<blockquote>
<p>如果把类看成一个C语言的结构体(struct),isa指针就是这个结构体的第一个成员变量,类的其他成员变量依次排列在结构体中</p>
</blockquote>
<p><strong>排列顺序:</strong></p>
<table>
<thead>
<tr>
<th>1</th>
<th>isa指针</th>
</tr>
</thead>
<tbody>
<tr>
<td>2</td>
<td>NSObject的成员变量</td>
</tr>
<tr>
<td>3</td>
<td>NSObject子类的成员变量</td>
</tr>
<tr>
<td>4</td>
<td>NSObject子类的子类的成员变量</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
</tr>
<tr>
<td>n-1</td>
<td>父类的成员变量</td>
</tr>
<tr>
<td>n</td>
<td>类本身的成员变量</td>
</tr>
</tbody>
</table>
<p>一个简单的继承的实例代码:</p>
<pre><code>@interface Father : NSObject {
int _father;
}
@end
@implementation Father
@end
@interface Child : Father {
int _child;
}
@end
@implementation Child
@end
</code></pre>
<p>在Xcode中,我们看到如下截图,这个结构与上面说的一致</p>
<p><img src="https://img2018.cnblogs.com/blog/793084/201910/793084-20191025162738960-482910900.png"></p>
<blockquote>
<p>因为对象在内存中的排布可以看成一个结构体,该结构体的大小并不能动态变化,所以无法在运行时动态地给对象增加成员变量。</p>
</blockquote>
<blockquote>
<p>对象的方法定义都保存在类的可变区域中。<br>
在下面的 <code>Objective-C 1.0</code> 中,我们可以看到方法的定义列表是一个名为 <code>methodLists</code> 的指针<br>
通过修改指针指向的指针的值,就可以动态的为某一个类增加成员方法,这也是 <code>Category</code> 实现的原理</p>
</blockquote>
<p><strong>Objective-C 1.0 objc_class代码</strong></p>
<pre><code>struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super _class
const char *name
long version
long info
long instance_size
struct objc_ivar_list *ivars
struvt objc_method_list **methodLists
struct objc_cache *cache
struct objc_protocol_list *protocols
#endIf
} OBJC2_UNAVAILABLE
</code></pre>
<h4 id="12-动态创建对象">1.2 动态创建对象</h4>
<pre><code>#import <objc/runtime.h>
...
- (void)dynamicCreateClass {
// 创建一个名为CustomView的类,它是UIView的子类
Class newClass = objc_allocateClassPair(, "CustomView", 0);
// 为这个类增加一个report的方法
class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:");
// 注册该类
objc_registerClassPair(newClass);
// 创建一个newClass的实例对象
id instanceOfNewClass = [ init];
// 调用report方法
;
}
void ReportFunction(id self, SEL _cmd) {
NSLog(@"This object is %p", self);
NSLog(@"Class is %@, and super is %@", , );
Class currentClass = ;
for (int i = 1; i < 5; i++) {
NSLog(@"Following the isa pointer %d times gives %@ = %p", i, currentClass, currentClass);
// 获取对象的isa指针所指向的对象
currentClass = object_getClass(currentClass);
}
NSLog(@"NSObject class is %@ = %p", , );
NSLog(@"NSObject meta class is %@ = %p", object_getClass(), object_getClass());
}
</code></pre>
<p>代码关键点:</p>
<ol>
<li><code>import runtime</code> 相关的头文件:<code>objc/runtime.h</code>。</li>
<li>使用 <code>objc_allocateClassPair</code> 方法创建新的类。</li>
<li>使用 <code>class_addMethod</code> 方法来给类增加新的方法。</li>
<li>使用 <code>objc_registerClassPair</code> 方法来注册新的类。</li>
<li>使用 <code>objc_getClass</code> 方法来获取对象的isa指针指向的对象。</li>
</ol>
<h4 id="13-方法交换method-swizzlingapi说明">1.3 方法交换(Method Swizzling)API说明</h4>
<p>Objective-C提供了以下API来动态替换类方法或实例方法的实现:</p>
<ul>
<li><code>class_replaceMethod</code> 替换类方法的定义</li>
</ul>
<pre><code>class_replaceMethod(Class_Nullable __unsafe_unretained cls, SEL_Nonnull name, IMP_Nonnull imp, const char * _Nullable types)
</code></pre>
<ul>
<li><code>method_exchangeImplementations</code> 交换两个方法的实现</li>
</ul>
<pre><code>method_exchangeImplementations(Method_Nonnull m1, Method_Nonnull m2)
</code></pre>
<ul>
<li><code>method_setImplementation</code> 设置一个方法的实现</li>
</ul>
<pre><code>method_setImplementation(Method_Nonnull m, IMP_Nonnull imp)
</code></pre>
<blockquote>
<p>比较:</p>
<ul>
<li><code>class_replaceMethod</code> 当类中没有找到要替换的原方法时,该方法会调用 <code>class_addMethod</code> 来为类增加一个新的方法,也正因为这样,<code>class_replaceMethod</code> 在调用时需要传入 <code>type</code> 参数,而 <code>method_exchangeImplementations</code> 和 <code>method_setImplementation</code> 都不需要</li>
<li><code>method_exchangeImplementations</code> 内部实现是获取到两个方法的实现,然后进行互换</li>
</ul>
<p><strong>文档如下图:</strong></p>
<p><img src="https://img2018.cnblogs.com/blog/793084/201910/793084-20191025162842070-648433925.png"></p>
</blockquote>
<p><strong>使用场景:</strong></p>
<ul>
<li><code>class_replaceMethod</code> 当需要替换的方法有可能不存在时,可以考虑使用该方法。</li>
<li><code>method_exchangeImplementations</code> 当需要交换两个方法的实现时使用。</li>
<li><code>method_setImplementation</code> 是最简单的用法,当仅仅需要为一个方法设置其实现方式时使用。</li>
</ul>
<h4 id="2tagged-pointer-对象">2、Tagged Pointer 对象</h4>
<h4 id="21-原有系统的问题">2.1 原有系统的问题</h4>
<blockquote>
<p><strong>32位程序过渡到64位存在的问题:</strong></p>
<ul>
<li>问题一:内存翻倍。<br>
在iOS数据类型中,很多数据类型所占内存都是根据CPU的位数决定的。那么,当程序从32位程序过渡到64位时,这些数据类型的内存就会翻倍。如下图所示:<br>
<img src="https://img2018.cnblogs.com/blog/793084/201910/793084-20191025162930015-1116965793.png"></li>
<li>问题二:效率问题。<br>
为了存储和访问一个NSNumber对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命周期。这些都给程序增加了额外的逻辑,造成运行效率上的损失,</li>
</ul>
</blockquote>
<h4 id="22-tagged-pointer-介绍">2.2 Tagged Pointer 介绍</h4>
<p>Tagged Pointer就是为了解决上述问题提出的。<br>
原理:将一个对象指针拆分为两部分。如下图:</p>
<p><img src="https://img2018.cnblogs.com/blog/793084/201910/793084-20191025163009670-762238299.png"></p>
<p>引入后,内存变化如下图:</p>
<p><img src="https://img2018.cnblogs.com/blog/793084/201910/793084-20191025163138281-202372.png"></p>
<blockquote>
<p>特点:</p>
<ol>
<li>专门用来存储小的对象,例如 <code>NSNumber</code> 和 <code>NSDate</code></li>
<li>指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象’皮‘的普通变量而已。所以,它的内存并不存储在堆中,也不需要 <code>malloc</code> 和 <code>free</code></li>
<li>在内存读取上有着以前3倍的效率,创建时比之前快106倍</li>
</ol>
</blockquote>
<p><strong>注:<code>Tagged Pointer</code> 并不是真正的对象,而是一个伪对象,没有 <code>isa</code> 指针</strong></p>
<h4 id="22-64位下-isa-指针优化">2.2 64位下 isa 指针优化</h4>
<h5 id="32位环境">32位环境:</h5>
<blockquote>
<p>对象的引用计数都保存在一个外部表中。</p>
</blockquote>
<p><code>Retain</code> 操作包含如下的5个步骤:</p>
<ol>
<li>获取全局的记录引用计数的 <code>hash</code> 表。</li>
<li>为了线程安全,给该 <code>hash</code> 表加锁。</li>
<li>查找到目标对象的引用计数值。</li>
<li>将该引用计数值加1,写回 <code>hash</code> 表。</li>
<li>给该 <code>hash</code> 表解锁。</li>
</ol>
<p>为了线程安全,需要对 <code>hash</code> 表进行加锁,从性能上看是非常差的。</p>
<h5 id="64位环境">64位环境:</h5>
<p>isa指针是64位。每个bit位含义如下图:</p>
<table>
<thead>
<tr>
<th>bit位</th>
<th>变量名</th>
<th>意义</th>
</tr>
</thead>
<tbody>
<tr>
<td>1 bit</td>
<td>indexed</td>
<td>0 表示普通的isa,1 表示 Tagged Pointer</td>
</tr>
<tr>
<td>1 bit</td>
<td>has_assoc</td>
<td>表示对象是否有过 associated 对象,如果没有,在析构释放内存时可以更快</td>
</tr>
<tr>
<td>1 bit</td>
<td>has_cxx_dtor</td>
<td>表示该对象是否有 C++ 或 ARC 的析构函数,如果没有,在析构释放内存时可以更快</td>
</tr>
<tr>
<td>30 bit</td>
<td>shiftcls</td>
<td>类的指针</td>
</tr>
<tr>
<td>9 bit</td>
<td>magic</td>
<td>其值固定为 0xd2,用于在调试时分辨对象是否未完成初始化</td>
</tr>
<tr>
<td>1 bit</td>
<td>weakly_referenced</td>
<td>表示该对象是否有过 weak 对象,如果没有,在析构释放内存时可以更快</td>
</tr>
<tr>
<td>1 bit</td>
<td>deallocating</td>
<td>表示该对象是否正在析构</td>
</tr>
<tr>
<td>1 bit</td>
<td>has_sidetable_rc</td>
<td>表示该对象的引用计数值是否大到无法直接在 isa 中保存</td>
</tr>
<tr>
<td>19 bit</td>
<td>extra_rc</td>
<td>表示该对象超过 1 的引用计数值,例如,如果该对象的引用计数是6,则 extra_rc 的值为5</td>
</tr>
</tbody>
</table>
<p>extra_rc 的19位 bit 用来保存对象的引用计数,这样对引用计数的操作只需要修改这个职责即可。</p>
<p><code>Retain</code> 操作包含如下的5个步骤:</p>
<ol>
<li>检查 isa 指针上面的标记位,看引用计数是否保存在 isa 变量中,如果不是,则使用以前的步骤,否则执行第2步。</li>
<li>检查当前对象是否正在释放,如果是,则不做任何事情。</li>
<li>增加该对象的引用计数,但是并不马上写回到 isa 变量中。</li>
<li>检查增加后的引用计数的值是否能够被19位表示,如果不是,则切换为以前的办法,否则执行第5步。</li>
<li>进行一个原子的写操作,将 isa 的值写回。</li>
</ol>
<h4 id="3block-对象模型">3、<code>block</code> 对象模型</h4>
<h4 id="31-定义">3.1 定义:</h4>
<p>在苹果的 llvm 项目的开源代码(https://llvm.org/svn/llvm-project/compiler-rt/tags/Apple/Libcompiler_rt-10/BlocksRuntime/Block_private.h)中,我们可以看到 <code>block</code> 的数据结构定义,如下图:</p>
<p><img src="https://img2018.cnblogs.com/blog/793084/201910/793084-20191025163205896-1833715738.png"></p>
<p>对应的结构体定义如下:</p>
<pre><code>struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables */
};
</code></pre>
<p>组成 <code>block</code> 实例的6个部分:</p>
<ol>
<li><code>isa</code> 指针,所有对象都有该指针,用于实现对象的相关的功能。</li>
<li><code>flags</code> 用于按 <code>bit</code> 位表示一些 <code>block</code> 的附加信息,在后面介绍的 <code>block copy</code> 的实现代码中可以看到该变量的使用。</li>
<li><code>reserved</code> 保留变量。</li>
<li><code>invoke</code> 函数指针,指向具体的 <code>block</code> 实现的函数调用地址。</li>
<li><code>descriptor</code> 表示该 <code>block</code> 的附加描述信息,主要是 <code>size</code> 的大小,以及 <code>copy</code> 和 dispose 函数的指针。</li>
<li><code>variable</code> <code>capture</code> 过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。</li>
</ol>
<h4 id="32-分类">3.2 分类:</h4>
<blockquote>
<p><code>block</code> 的类型:</p>
<ol>
<li><code>_NSConcreteGlobalBlock</code> 全局的静态 <code>block</code>,不会访问任何外部变量。</li>
<li><code>_NSConcreteStackBlock</code> 保存在栈中的 <code>block</code>,当函数返回时会被销毁。</li>
<li><code>_NSConcreteMallocBlock</code> 保存在堆中的 <code>block</code>,当引用计数为 0 时会被销毁。</li>
</ol>
</blockquote>
<p><strong>注:用 <code>clang</code> 分析 <code>block</code> 实现</strong></p>
<blockquote>
<p><code>clang</code> 提供了一个命令,可以将 <code>Objective-C</code> 的源码改写成C语言。<br>
命令是:<code>clang -rewrite-objc block.c</code></p>
</blockquote>
<h4 id="321-nsconcreteglobalblock-类型的-block-的实现">3.2.1 <code>NSConcreteGlobalBlock</code> 类型的 <code>block</code> 的实现</h4>
<p>创建一个名字为 block1.c 的源文件,文件实现:</p>
<pre><code>#include <stdio.h>
int main(int argc, char const *argv[]) {
^{ printf("Hello, World!\n"); } ();
return 0;
}
</code></pre>
<p>在命令行中输入 <code>clang -rewrite-objc block1.c</code>,即可在目录中看到 clang 输出了一个名为 ”block1.cpp” 的文件,这个文件就是 block 在C语言中的实现。<br>
关键代码引用如下:</p>
<pre><code>...
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
...
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Hello, World!\n"); }
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char const *argv[])
{
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)) ();
return 0;
}
</code></pre>
<p>代码中,<code>__main_block_impl_0</code> 就是该 block 的实现,从中我们可以看出:</p>
<ol>
<li>一个 <code>block</code> 实际是一个对象,它主要由一个 <code>isa</code>、一个 <code>impl</code> 和一个 <code>descriptor</code> 组成。</li>
<li>由于这里没有开启 <code>ARC</code>,所以我们看到 <code>isa</code> 的指向还是 <code>_NSConcreteStackBlock</code>。但在开启 <code>ARC</code> 时,<code>block</code> 应该是 <code>_NSConcreteGlobalBlock</code> 类。</li>
<li><code>impl</code> 是实际函数指针,本例中,它指向 <code>__main_block_func_0</code>。这里的 <code>impl</code> 相当于之前提到的 <code>invoke</code> 变量,只是 <code>clang</code> 编译器对变量的命名不一样而已。</li>
<li><code>descriptor</code> 是用于描述当前这个 <code>block</code> 的附加信息的,包括结构体的大小,需要 <code>capture</code> 和 <code>dispose</code> 的变量列表等。<br>
结构体大小需要保存到原因是,每个 <code>block</code> 会 <code>capture</code> 一些变量,这里变量会加到 <code>__main_block_impl_0</code> 这个结构体中,使其体积变大。</li>
</ol>
<p>具体文件见:https://github.com/AlonerOwl/OC_Block/tree/master/NSConcreteGlobalBlock</p>
<h4 id="322-nsconcretestackblock-类型的-block-的实现">3.2.2 <code>NSConcreteStackBlock</code> 类型的 <code>block</code> 的实现</h4>
<p>创建一个名字为 block1.c 的源文件,文件实现:</p>
<pre><code>#include <stdio.h>
int main(int argc, char const *argv[]) {
int a = 100;
void (^block2)(void) = ^{ // block 实现
printf("%d\n", a);
};
block2();
return 0;
}
</code></pre>
<p>clang 后:</p>
<pre><code>struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("%d\n", a);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char const *argv[]) {
int a = 100;
void (*block2)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);
return 0;
}
</code></pre>
<p>在本例中,我们可以看到:</p>
<ol>
<li>本例中,<code>isa</code> 指向 <code>_NSConcreteStackBlock</code>,说明这是一个分配在栈上的实例。</li>
<li><code>__main_block_impl_0</code> 中增加一个变量 <code>a</code>,在 <code>block</code> 中引用的变量 <code>a</code>,实际是在声明 <code>block</code> 时,被复制到 <code>__main_block_impl_0</code> 结构体中的那个变量 <code>a</code>。</li>
<li><code>__main_block_impl_0</code> 中由于增加一个变量 <code>a</code>,所以结构体变大了,该结构体大小被写在了 <code>__main_block_desc_0 中</code>。</li>
</ol>
<p>我们修改上面的源码,在变量前面增加 <code>__block</code> 关键字:</p>
<pre><code>#include <stdio.h>
int main(int argc, char const *argv[]) {
__block int i = 1024;
void (^block2)(void) = ^{ // block 实现
printf("%d\n", i);
i = 1023;
};
block2();
return 0;
}
</code></pre>
<p>clang 后,与之前差异相当大:</p>
<pre><code>struct __Block_byref_i_0 {
void *__isa;
__Block_byref_i_0 *__forwarding;
int __flags;
int __size;
int i;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_i_0 *i; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_i_0 *i = __cself->i; // bound by ref
printf("%d\n", (i->__forwarding->i));
(i->__forwarding->i) = 1023;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, char const *argv[]) {
__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024};
void (*block2)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));
((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);
return 0;
}
</code></pre>
<p>从代码中我们可以看到:</p>
<ol>
<li>源码中增加了一个名为 <code>__Block_byref_i_0</code> 的结构体,用于保存我们要 <code>capture</code> 并且修改的变量 <code>i</code>。</li>
<li><code>__main_block_impl_0</code> 中引用的是 <code>__Block_byref_i_0</code> 的结构体指针,这样就可以起到修改外部变量的作用。</li>
<li><code>__Block_byref_i_0</code> 的结构体带有 <code>isa</code>,说明它也是一个对象。</li>
<li>我们需要负责 <code>__Block_byref_i_0</code> 结构体相关的内存管理,所有 <code>__main_block_desc_0</code> 中增加了 <code>copy</code> 和 <code>dispose</code> 函数指针,用于在调用前后修改相应变量的引用计数。</li>
</ol>
<p>具体文件见:https://github.com/AlonerOwl/OC_Block/tree/master/NSConcreteStackBlock</p>
<blockquote>
<p><strong>总结:</strong><br>
<code>block</code> 对于外部变量的使用,非 <code>__block</code> 修饰的变量,直接将其复制到 <code>block</code> 数据结构中来实现访问;<code>__block</code> 修饰的变量,复制这个变量的引用地址来实现访问的。</p>
</blockquote>
<h4 id="323-nsconcretemallocblock-类型的-block-的实现">3.2.3 <code>NSConcreteMallocBlock</code> 类型的 <code>block</code> 的实现</h4>
<p>NSConcreteMallocBlock 类型的 block 通常不会在源码中直接出现,只有当一个 block 被调用其 copy 方法的时候,系统才会将这个 block 复制到堆中,从而产生 NSConcreteMallocBlock 类型的 block。</p>
<h5 id="注在-arc-开启的情况下将只会存在-nsconcreteglobalblock-和-nsconcretemallocblock-类型的-block原来的-nsconcretestackblock-会被-nsconcretemallocblock-的进行替代">注:在 <code>ARC</code> 开启的情况下,将只会存在 <code>NSConcreteGlobalBlock</code> 和 <code>NSConcreteMallocBlock</code> 类型的 <code>block</code>。原来的 <code>NSConcreteStackBlock</code> 会被 <code>NSConcreteMallocBlock</code> 的进行替代。</h5><br><br>
来源:https://www.cnblogs.com/gfxxbk/p/11738758.html
頁:
[1]