P.S.(该系列文章是个人学习总结,拿出来和大家讨论,水平有限,如有错误,特别、非常、极其欢迎批评和指正!)
开始之前,先放一个链接,这个网站可以查看不同版本 Qt 相关的源码,不调试的话用这个就很方便。Qt源码浏览
1 疑问
Qt 作为跨平台的GUI框架,在实际项目中应用广泛,在日常的使用中,随手使用的一些机制(如著名的信号槽机制),属性(如Property系统),以及重载各种事件函数来完成定制化,有时不禁好奇这些内容是怎么实现的。该系列文章不适合作为 Qt 的入门文章,适合有一定 Qt 使用经验,想了解 Qt 内部核心机制的朋友们。
是否好奇过,为什么在 Qt 的框架下,我们只需要通过简单的信号槽宏连接两个对象的方法,就可以实现类似观察者的通信方式——甚至当前类并没有存另一个类的任何信息。
带着好奇,我查看了经典的SINGAL()
和SLOT()
宏定义,我发现这个宏就做了一个事情,把我们的信号和槽的方法包装为一个字符串!那个qFlagLocation
可以看到,就是进去转了一圈。
# define SLOT(a) qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a) qFlagLocation("2"#a QLOCATION)
const char *qFlagLocation(const char *method)
{
QThreadData *currentThreadData = QThreadData::current(false);
if (currentThreadData != nullptr)
currentThreadData->flaggedSignatures.store(method);
return method;
}
这里没有发现猫腻,那么猫腻是不是在connect
方法中呢?
static QMetaObject::Connection connect(const QObject *sender, const char *signal,
const QObject *receiver, const char *member, Qt::ConnectionType = Qt::AutoConnection);
可以看到,这里面确实只利用了前面包装的字符串——即函数名,问题是,你见过 C 中有如下的调用吗?
代码语言:c 复制pMyclass->"method1";
//或者
myClass."method2";
那么,Qt 只是拿两个方法名就能完成调用,是怎么做到的呢?素朴的想法是,一定是根据某种方法把字符串转换为对应对象方法,在通过方法调用来完成,但是 C 本身显然不提供这个能力,Java 中有类似反射的概念可以完成这个任务。
所以推测,Qt 大概率是采用某种方法拿到了方法和函数名的映射数据,从而完成转换,这部分数据我们暂且称为元数据。
2 元数据和元对象
什么是元数据?
元数据是描述数据的数据,试想一下,我们会怎么描述一个类 MyClass:
代码语言:c 复制class MyClass : public Object
{
public:
MyClass();
~MyClass();
enum Type
{
//...
};
public:
virtual void fool() override;
void bar();
//...
};
- 这个类的类名为MyClass
- 继承了一个基类 Object
- 有一个无参的构造函数和一个析构函数
- 实现了继承来的一个虚方法
- 自己有一个名为bar的public方法
- 内定义了一个枚举类型
- ...
上述描述内容就是元数据,用来描述我们声明的一个class,如果我们把以上数据封装为一个类,我们简单的认为这个类就是元对象。
3 额外的话题:为什么需要元对象系统
3.1 场景和问题
1)类型转换
面型对象的应用场景中我们经常操作一个指向派生类的基类指针,利用面向对象的多态特性,可以大大简化我们的编码方式,也是各种代码设计,设计模式中的基础。但是不可避免的,我们会遇到需要知道一个对象具体类型的时候(比如在一段处理 Object 的逻辑里面,如果这个类型是 MyClass,我们需要做一些特殊处理),这时候该怎么办呢?
2)对象间通信
Qt 中最有特点的便是对象间的通讯机制-信号槽系统,这点在GUI程序尤为重要,使用起来很方便,绑定对象的信号和槽,当信号发送时,槽函数得到响应。如果使用 C 的能力,我们要怎么做呢?
3)运行时增加属性
如果,我想在运行时根据当前的上下文为一个对象增加或者删除属性,并且要做到在其他地方使用的时候无感——就像这个属性原来就声明在类中一样,在原生的 C 中,怎么办?
4)...
3.2 C 的解决方案
针对场景1),我们当然可以使用 dynamic_cast 去尝试,但我想对于所有 C 的开发者来讲,我们都会有意避免使用动态类型转换,尤其是继承深度不断增长时,大量而频繁的 dynamic_cast 不可避免的使程序变慢。
对于场景2),我们可以使用回调函数或者函数对象,但是类型安全检查让人头秃,各种typedef也不好看;我们也可以使用观察者模式,当一个对象的行为发生变化时,更新另一个对象的状态,但是发现了吗,这个地方是紧耦合(一定要知道具体的类型),而且对于函数签名限制死了,更通用的说法是,对于 RTTI(运行时类型信息), C 并没有提供很好的支持,没有一种反射机制,可以让我们运行时得知一个类的描述(继承关系,成员函数...), C 是静态语言,这些信息在编译器存在,但是运行期是没有的。
对于场景3),无解,最起码以我有限的开发经验没想到办法。
...
那么该如何解决这个问题呢?Qt 给出的答案是基于 Qt 元对象系统的一系列机制。
4 朴素的元对象系统
Qt 的元对象系统发展这么久,完善是真的完善,代码多也是真的多!在迷失于复杂繁琐的源代码中之前,不妨先来设计一个简单的元对象系统来帮助我们理解思想。
4.1 元对象声明
联系前面的元数据的说明,朴素的想法是我们可以用另一个对象来描述这些信息,即元对象,在运行时通过这个对象来获取相关的具体类型等。
根据我们的需要,元对象应该具有以下信息
- 类型名
- 继承的父类信息
- 成员函数的信息
- 内部定义的枚举变量可能也是需要的
- ...
看起来像是这样
代码语言:c 复制class MetaObject
{
public:
// 其他成员函数
// ...
private:
// 简单起见,直接用对象了
ClassInfo m_info;
ClassInfo m_superClass;
ClassMethod m_methods;
ClassEnums m_enums;
};
4.2 对C 扩展
为了使我们能在软件系统中有效的管理,我们需要对MyClass做一些拓展,现在MyClass看上去像这样
代码语言:c 复制// MyClass.h
class MyClass : public Object
{
// ... 和之前一样
// 重写一个来自Object的虚方法
virtual const MetaObject *metaObject() const override;
static const MetaObject staticMetaObject; // 一个静态成员
};
现在,只要这个数据能够正确初始化,如果我们需要,我们就可以借助多态的特性,通过接口来获得这个类的相关信息了。
4.3 初始化元对象
那么问题来了,怎么初始化这个变量呢,C 作为静态语言,想要获取这些编译期有关的信息,我们只能选择在编译时或者编译前来做这件事,直觉告诉我们,我们要做编译器之前来做这件事,有两个显而易见的原因
- 不要妄图修改编译器,成本巨大且危险
- 直接修改编译器显示不是用户能接受的方式
当然可以手动编写这个文件,把类的信息一个个提炼出来,但是那样太不程序员了,我们需要写一段程序,在编译器之前来做这个事情(你可以把它当成一段生成代码的脚本),我们可以这样做:
- 在我们写的类里面加上一个标记,来表示该类使用了元对象,需要处理并正确初始化 MetaObejct,我们这里假设就用 DEBUG_OBJ 来表示
- 运行我们的程序,如果在某个文件里面发现了标记,解析这个文件,获取他的类型信息(ClassInfo),方法信息(ClassMethod),继承信息等
- 脚本生成了一个 moc_MyClass.cpp 文件,用上述信息初始化 MetaObject,类似于下面这样
// 由脚本生成的文件
// moc_MyClass.cpp
#include "MyClass.h"
// 这里是脚本解析原来头文件生成的数据
// 解析了类的名称,成员,继承关系等等
// ...
const MetaObject MyClass::staticMetaObject = {
// 用解析来的数据来初始化元对象内容
};
const MetaObject *MyClass::metaObject() const
{
return &staticMetaObject;
}
Done!
然后把这个文件也为做源文件一起编译就行了。
4.4 使用元对象
现在再回头来看前面的问题
1)现在直接通过虚函数多态性质拿到 MetaObject,再拿到元数据,比较两个类名是不是一致即可,如果我们采用静态的字符串数组来存类名,甚至我们不需要比较字符串是否一致,只需要比较字符串指针是否相同就可以了。
2)现在直接绑定两个对象的方法字符串即可,我们可以在 MetaObject 提供两各方法
- 检查这两个字符串是否是类的方法(ClassMethod中有没有这个字符串以及参数检查),以判断绑定是否能成功
- 一个统一的调用形式,内部根据字符串来调用相关方法
3)现在你可添加属性,实际添加到元数据中,而存取就像你调用get,set方法一样自然
大功告成,至此,一个丑陋的、不周全的乞丐版元对象系统就设计好了!
5 Qt的解决方案
以下关于元数据部分的内容参考了下面两篇博客,可以作为延伸阅读。
RunningSnail:深入了解Qt(二)之元对象系统(Meta-Object System)
天山老妖S:Qt高级——Qt信号槽机制源码解析
来看一下成熟的解决方案——Qt的元对象系统。
Qt官方文档 的描述是: Qt's meta-object system provides the signals and slots mechanism for inter-object communication, run-time type information, and the dynamic property system. 即qt元对象系统主要提供了三个能力
- 对象间通信(信号槽机制)
- 运行时信息(类似反射机制)
- 动态的属性系统
根据我们之前分析的乞丐版元对象系统的思想,下面来看以下 Qt 元对象系统是如何构建的,这里笔者环境:win平台vs2017,Qt 版本 5.6.3,为了统一,可以查看前面的在线浏览代码的网站
5.1 Qt 元对象模型
首先看一下 Qt 的元对象里面有什么,Qt 元对象声明位于includeQtCoreqobjectdefs.h
中,头文件中的部分大概有200行左右,但是看出来其中是有明显的划分的,在元对象中定义了用来存放元数据的地方(源文件的604-612)行,我们可以看到其中存放的元数据的结构
- 元数据以字符串和数组的形式存放在私有的结构体中,
// qobjectdefs.h
struct Q_CORE_EXPORT QMetaObject
{
// ...
// line 604-612(在本地5.6.3版本是line 502 - 510,以在线的为准吧,方便查看)
struct { // private data
const QMetaObject *superdata; // 父类的元对象指针
const QByteArrayData *stringdata; // 元数据的字符串数据
const uint *data; // 元对象的数据信息
typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);
StaticMetacallFunction static_metacall; // 一个函数指针,信号槽机制会用到
const QMetaObject * const *relatedMetaObjects; // ...还不清楚这个有什么用
void *extradata; //reserved for future use
} d;
};
除了相关的类名,继承关系等,查看省略的那部分代码中的 line 351 ~ line 375 ,我们可以发现这些成员函数显然被划分为以下几部分,并提供了相关的获取,查找等方法
- classInfo 相关
- method 相关,包括信号和槽
- enumerator 相关
- property 相关
例如关于method部分的:观察上面的元数据,不难理解这里的 offset,index 实际上再求这些数据再字串中的位置,以及根据字符串查找索引值(现在不难理解为什么我们的信号槽通过字符串就可以找到方法来调用了吧)。
QMetaMethod 定义在 includeQtCoreqmetaobject.h
内,描述了函数的签名,包括返回值,参数类型,参数个数,访问权限等等,可以自行查看
int methodOffset() const;
int methodCount() const;
int indexOfConstructor(const char *constructor) const;
int indexOfMethod(const char *method) const;
int indexOfSignal(const char *signal) const;
int indexOfSlot(const char *slot) const;
QMetaMethod constructor(int index) const;
QMetaMethod method(int index) const;
5.2 对 C 的扩展
还记得我们那个简陋的元对象系统是如何完成扩展和初始化的吗
- 在我们写的类里面加上一个标记,来表示该类使用了元对象系统
- 在编译之前运行我们的解析程序,如果在某个文件里面发现了标记,解析这个文件,获取元数据信息
- 生成一个 moc_MyClass.cpp 文件,用上述信息初始化,然后让这个文件一起参与编译
对应我们工作中写的类,我们会在类中加入 Q_OBJECT 宏来表示这个类需要使用元对象系统的特性,如果有需要,还有另外的 Q_CLASSINFO 和 Q_ENUMS 等宏可以使用,用来可选的把相关的信息记录到元数据中,以免不需要的部分使代码体积过度膨胀
现在假设我们有一个 MyClass 文件,继承自 QObject, 内含一个枚举变量,这里还添加了一个类的信息,以 key - value 的形式
代码语言:c 复制// Myclass.h
#pragma once
#include "QObject"
class Myclass : public QObject
{
Q_OBJECT
Q_CLASSINFO("Owner", "Frank")
public:
Myclass();
~Myclass();
enum ETest {
EValue1,
EValue2
};
Q_ENUMS(ETest)
public slots:
void onValueChanged() {}
signals:
void vualeChanged();
};
我们在简陋的元对象系统里面
- 用一个变量来记录了元对象
- 以 override 一个虚方法的形式来提供获取这个对象的入口
这些工作现在由Q_OBJECT
宏定义来完成(甚至更多),这个宏定义了很多东西,下面列出了其展开后的一部分内容,可以看到,其中定义了类的静态变量 QMetaObject,提供了获取的方法,并且 override 了另外的几个虚方法,这些方法使我们的类可以很好的融入 Qt 的框架,例如使用 qobject_cast,提供了更好的动态类型转换
#define Q_OBJECT
public:
QT_WARNING_PUSH
Q_OBJECT_NO_OVERRIDE_WARNING
static const QMetaObject staticMetaObject;
virtual const QMetaObject *metaObject() const;
virtual void *qt_metacast(const char *);
virtual int qt_metacall(QMetaObject::Call, int, void **);
QT_TR_FUNCTIONS
5.3 MOC 文件
接下来就是完成初始化的过程了,在编译前,会先执行 moc 程序(meta object compiler),生成 moc_MyClass.cpp,这个过程可以看作是找到有标记的文件,解析类的描述信息,生成的文件如下,其中删掉了一些不关键的自动生成的信息。这个文件很长,可以直接拖到文件后面(还不清楚怎么设置代码块可以折叠,求教!)
代码语言:c 复制// ...
#include "../../../myclass.h"
#include <QtCore/qbytearray.h>
#include <QtCore/qmetatype.h>
// ...
QT_BEGIN_MOC_NAMESPACE
struct qt_meta_stringdata_Myclass_t {
QByteArrayData data[9];
char stringdata0[71];
};
#define QT_MOC_LITERAL(idx, ofs, len)
Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len,
qptrdiff(offsetof(qt_meta_stringdata_Myclass_t, stringdata0) ofs
- idx * sizeof(QByteArrayData))
)
static const qt_meta_stringdata_Myclass_t qt_meta_stringdata_Myclass = {
{
QT_MOC_LITERAL(0, 0, 7), // "Myclass"
QT_MOC_LITERAL(1, 8, 5), // "Owner"
QT_MOC_LITERAL(2, 14, 5), // "Frank"
QT_MOC_LITERAL(3, 20, 12), // "vualeChanged"
QT_MOC_LITERAL(4, 33, 0), // ""
QT_MOC_LITERAL(5, 34, 14), // "onValueChanged"
QT_MOC_LITERAL(6, 49, 5), // "ETest"
QT_MOC_LITERAL(7, 55, 7), // "EValue1"
QT_MOC_LITERAL(8, 63, 7) // "EValue2"
},
"Myclass Owner Frank vualeChanged "
"onValueChanged ETest EValue1 EValue2"
};
#undef QT_MOC_LITERAL
static const uint qt_meta_data_Myclass[] = {
// content:
7, // revision
0, // classname
1, 14, // classinfo
2, 16, // methods
0, 0, // properties
1, 28, // enums/sets
0, 0, // constructors
0, // flags
1, // signalCount
// classinfo: key, value
1, 2,
// signals: name, argc, parameters, tag, flags
3, 0, 26, 4, 0x06 /* Public */,
// slots: name, argc, parameters, tag, flags
5, 0, 27, 4, 0x0a /* Public */,
// signals: parameters
QMetaType::Void,
// slots: parameters
QMetaType::Void,
// enums: name, flags, count, data
6, 0x0, 2, 32,
// enum data: key, value
7, uint(Myclass::EValue1),
8, uint(Myclass::EValue2),
0 // eod
};
void Myclass::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
if (_c == QMetaObject::InvokeMetaMethod) {
Myclass *_t = static_cast<Myclass *>(_o);
Q_UNUSED(_t)
switch (_id) {
case 0: _t->vualeChanged(); break;
case 1: _t->onValueChanged(); break;
default: ;
}
} else if (_c == QMetaObject::IndexOfMethod) {
int *result = reinterpret_cast<int *>(_a[0]);
void **func = reinterpret_cast<void **>(_a[1]);
{
typedef void (Myclass::*_t)();
if (*reinterpret_cast<_t *>(func) == static_cast<_t>(&Myclass::vualeChanged)) {
*result = 0;
return;
}
}
}
Q_UNUSED(_a);
}
const QMetaObject Myclass::staticMetaObject = {
{ &QObject::staticMetaObject, qt_meta_stringdata_Myclass.data,
qt_meta_data_Myclass, qt_static_metacall, Q_NULLPTR, Q_NULLPTR}
};
const QMetaObject *Myclass::metaObject() const
{
return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}
void *Myclass::qt_metacast(const char *_clname)
{
if (!_clname) return Q_NULLPTR;
if (!strcmp(_clname, qt_meta_stringdata_Myclass.stringdata0))
return static_cast<void*>(const_cast< Myclass*>(this));
return QObject::qt_metacast(_clname);
}
int Myclass::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = QObject::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
if (_id < 2)
qt_static_metacall(this, _c, _id, _a);
_id -= 2;
} else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
if (_id < 2)
*reinterpret_cast<int*>(_a[0]) = -1;
_id -= 2;
}
return _id;
}
// SIGNAL 0
void Myclass::vualeChanged()
{
QMetaObject::activate(this, &staticMetaObject, 0, Q_NULLPTR);
}
QT_END_MOC_NAMESPACE
如果这个类足够复杂,你会发现这个生成的文件很长很长,这一小节我们先来看有关元数据一部分,其他的一些方法实现和信号槽有关,后续再进行讨论。
QMetaObject
对象的私有数据中有几个变量需要初始化首先是const QByteArrayData *stringdata; // 元数据的字符串数据
,moc文件中解析来的数据如下。
// 定义了一个数据结构
struct qt_meta_stringdata_Myclass_t {
QByteArrayData data[9];
char stringdata0[71];
};
#define QT_MOC_LITERAL(idx, ofs, len)
Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len,
qptrdiff(offsetof(qt_meta_stringdata_Myclass_t, stringdata0) ofs
- idx * sizeof(QByteArrayData))
)
// 元数据,用来初始化元对象中的stringdata部分
static const qt_meta_stringdata_Myclass_t qt_meta_stringdata_Myclass = {
{
QT_MOC_LITERAL(0, 0, 7), // "Myclass"
QT_MOC_LITERAL(1, 8, 5), // "Owner"
QT_MOC_LITERAL(2, 14, 5), // "Frank"
QT_MOC_LITERAL(3, 20, 12), // "vualeChanged"
QT_MOC_LITERAL(4, 33, 0), // ""
QT_MOC_LITERAL(5, 34, 14), // "onValueChanged"
QT_MOC_LITERAL(6, 49, 5), // "ETest"
QT_MOC_LITERAL(7, 55, 7), // "EValue1"
QT_MOC_LITERAL(8, 63, 7) // "EValue2"
},
"Myclass Owner Frank vualeChanged "
"onValueChanged ETest EValue1 EValue2"
};
#undef QT_MOC_LITERAL
其次是const uint *data; // 元对象的数据信息
部分,moc文件中解析来的数据如下。
// 元数据,用来初始元对象的data部分
static const uint qt_meta_data_Myclass[] = {
// content:
7, // revision
0, // classname
1, 14, // classinfo
2, 16, // methods
0, 0, // properties
1, 28, // enums/sets
0, 0, // constructors
0, // flags
1, // signalCount
// classinfo: key, value
1, 2,
// signals: name, argc, parameters, tag, flags
3, 0, 26, 4, 0x06 /* Public */,
// slots: name, argc, parameters, tag, flags
5, 0, 27, 4, 0x0a /* Public */,
// signals: parameters
QMetaType::Void,
// slots: parameters
QMetaType::Void,
// enums: name, flags, count, data
6, 0x0, 2, 32,
// enum data: key, value
7, uint(Myclass::EValue1),
8, uint(Myclass::EValue2),
0 // eod
};
// ...
用上述数据初始化元对象。
代码语言:c 复制const QMetaObject Myclass::staticMetaObject = {
{ &QObject::staticMetaObject, qt_meta_stringdata_Myclass.data,
qt_meta_data_Myclass, qt_static_metacall, Q_NULLPTR, Q_NULLPTR}
};
上述代码片段中初始化动作在最后,可以看到这里使用了前面定义的静态变量来进行元数据的初始化,这些静态的数据就是原 moc 编译器运行之后得到的描述一个类的相关数据,这些数据可以发现实际就是用来初始化 private 结构中的哪些部分的。
怎么用呢?仔细观察我们不难发现,在 qt_meta_stringdata_Myclass_t
中有两个成员,一个成员保存了一个索引的数组,一个成员保存了一个字符串数组,静态变量 qt_meta_stringdata_Myclass
的初始化过程中我们可以发现,类型的信息被描述为索引号 起始位置偏移值 长度
的形式。
例如: 所以为1的这项描述,对应元数据的字符串起始位置在第0位,长度为7,从字符串中可以看到,这部分取出来正好是类的名称(MyClass)。
那么现在问题是我们怎么知道我们的这些元数据索引是多少呢,这就需要用到另一个 qt_meta_data_Myclass 的数据了。
注意content部分,这部分对应了一个元对象的私有结构,struct QMetaObjectPrivate
中,位于`qtbase/src/corelib/kernel/qmetaobject_p.h
`中
代码语言:c 复制// 解析出来的内容对象的位置,索引,个数等信息,配合下面的数据使用
// content:
7, // revision
0, // classname classname在所以为0的位置
1, 14, // classinfo count,offset 有一个classinfo,在该数组index为14的地方
2, 16, // methods 和上面一样,两个方法,index为16
0, 0, // properties
1, 28, // enums/sets
0, 0, // constructors
0, // flags
1, // signalCount
例如类型的名称MyClass,这里的意思是:该信息的索引为0,对应字符串数组中,相对起始位置为 0 的地址开始,长度为7,从下面的字符串数组可以看出,确实如此。
代码语言:c 复制// 解析的相关字符串
static const qt_meta_stringdata_Myclass_t qt_meta_stringdata_Myclass = {
{
QT_MOC_LITERAL(0, 0, 7), // "Myclass"
QT_MOC_LITERAL(1, 8, 5), // "Owner"
QT_MOC_LITERAL(2, 14, 5), // "Frank"
QT_MOC_LITERAL(3, 20, 12), // "vualeChanged"
QT_MOC_LITERAL(4, 33, 0), // ""
QT_MOC_LITERAL(5, 34, 14), // "onValueChanged"
QT_MOC_LITERAL(6, 49, 5), // "ETest"
QT_MOC_LITERAL(7, 55, 7), // "EValue1"
QT_MOC_LITERAL(8, 63, 7) // "EValue2"
},
"Myclass Owner Frank vualeChanged "
"onValueChanged ETest EValue1 EValue2"
};
可以看下面这张图
另外一个值得注意的是,如前面所说,如果我们没有使用像Q_ENUMS
, Q_CLASSINFO
等类似的宏,元数据里面不会生产相应信息,也避免了数据过多引起的代码膨胀。
那么到这里,Qt 就把一个类的元数据和元对象都构建好了,这套系统后面会被用于信号槽机制和属性系统等,我们下次再做讨论。
6 小结
Qt 中的元对象系统,简单的可以分为以下几步
- 在继承 QObject 的类中使用 Q_OBJECT 宏,该宏定义了元对象和相关的方法
- 进行 C 编译前,Qt 会运行 moc,解析带有 Q_OBJECT 宏的相关类的信息,生成moc文件,得到元数据并构造元对象
- 将生成的文件和源文件一起编译