内容简介:在iOS程序中会用到很多系统的动态库,这些动态库都是动态加载的。所有iOS程序共用一套系统动态库,在程序开始运行时才会开始链接动态库。除了在项目设置里显式出现的动态库外,还会有一些隐式存在的动态库。例如
<简书 — 刘小壮> https://www.jianshu.com/p/4fb2d7014e9e
程序加载过程
在iOS程序中会用到很多系统的动态库,这些动态库都是动态加载的。所有iOS程序共用一套系统动态库,在程序开始运行时才会开始链接动态库。
除了在项目设置里显式出现的动态库外,还会有一些隐式存在的动态库。例如 objc 和 Runtime 所属的 libobjc.dyld 和 libSystem.dyld ,在 libSystem 中包含常用的 libdispatch(GCD) 、 libsystem_c (C语言基础库)、 libsystem_blocks(Block) 等。
使用动态库的优点:
App
加载过程
在应用程序启动后,由 dyld(the dynamic link editor) 进行程序的初始化操作。大概流程就像下面列出的步骤,其中第3、4、5步会执行多次,在 ImageLoader 加载新的 image 进内存后就会执行一次。
- 在引用程序启动后,由
dyld将应用程序加载到二进制中,并完成一些文件的初始化操作。 -
Runtime向dyld中注册回调函数。 - 通过
ImageLoader将所有image加载到内存中。 -
dyld在image发生改变时,主动调用回调函数。 -
Runtime接收到dyld的函数回调,开始执行map_images、load_images等操作,并回调+load方法。 - 调用
main()函数,开始执行业务代码。
ImageLoader 是 image 的加载器, image 可以理解为编译后的二进制。
下面是在 Runtime 的 map_images 函数打断点,观察回调情况的汇编代码。可以看出,调用是由 dyld 发起的,由 ImageLoader 通知 dyld 进行调用。
关于 dyld 我并没有深入研究,有兴趣的同学可以到 Github 上下载源码研究一下。
动态加载
一个OC程序可以在运行过程中动态加载和链接新类或 Category ,新类或 Category 会加载到程序中,其处理方式和其他类是相同的。动态加载还可以做许多不同的事,动态加载允许应用程序进行自定义处理。
OC提供了 objc_loadModules 运行时函数,执行 Mach-O 中模块的动态加载,在上层 NSBundle 对象提供了更简单的访问 API 。
map images
在 Runtime 加载时,会调用 _objc_init 函数,并在内部注册三个函数指针。其中 map_images 函数是初始化的关键,内部完成了大量 Runtime 环境的初始化操作。
在 map_images 函数中,内部也是做了一个调用中转。然后调用到 map_images_nolock 函数,内部核心就是 _read_images 函数。
void _objc_init(void)
{
// .... 各种init
_dyld_objc_notify_register(↦_images, load_images, unmap_image);
}
void map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
rwlock_writer_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
void map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
}
复制代码
在 _read_images 函数中完成了大量的初始化操作,函数内部代码量比较大,下面是精简版带注释的源代码。
先整体梳理一遍 _read_images 函数内部的逻辑:
- 加载所有类到类的
gdb_objc_realized_classes表中。 - 对所有类做重映射。
- 将所有
SEL都注册到namedSelectors表中。 - 修复函数指针遗留。
- 将所有
Protocol都添加到protocol_map表中。 - 对所有
Protocol做重映射。 - 初始化所有非懒加载的类,进行
rw、ro等操作。 - 遍历已标记的懒加载的类,并做初始化操作。
- 处理所有
Category,包括Class和Meta Class。 - 初始化所有未初始化的类。
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
header_info *hi;
uint32_t hIndex;
size_t count;
size_t i;
Class *resolvedFutureClasses = nil;
size_t resolvedFutureClassCount = 0;
static bool doneOnce;
TimeLogger ts(PrintImageTimes);
#define EACH_HEADER \
hIndex = 0; \
hIndex < hCount && (hi = hList[hIndex]); \
hIndex++
if (!doneOnce) {
doneOnce = YES;
// 实例化存储类的哈希表,并且根据当前类数量做动态扩容
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
}
// 由编译器读取类列表,并将所有类添加到类的哈希表中,并且标记懒加载的类并初始化内存空间
for (EACH_HEADER) {
if (! mustReadClasses(hi)) {
continue;
}
bool headerIsBundle = hi->isBundle();
bool headerIsPreoptimized = hi->isPreoptimized();
/** 将新类添加到哈希表中 */
// 从编译后的类列表中取出所有类,获取到的是一个classref_t类型的指针
classref_t *classlist = _getObjc2ClassList(hi, &count);
for (i = 0; i < count; i++) {
// 数组中会取出OS_dispatch_queue_concurrent、OS_xpc_object、NSRunloop等系统类,例如CF、Fundation、libdispatch中的类。以及自己创建的类
Class cls = (Class)classlist[i];
// 通过readClass函数获取处理后的新类,内部主要操作ro和rw结构体
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
// 初始化所有懒加载的类需要的内存空间
if (newCls != cls && newCls) {
// 将懒加载的类添加到数组中
resolvedFutureClasses = (Class *)
realloc(resolvedFutureClasses,
(resolvedFutureClassCount+1) * sizeof(Class));
resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
}
}
}
// 将未映射Class和Super Class重映射,被remap的类都是非懒加载的类
if (!noClassesRemapped()) {
for (EACH_HEADER) {
// 重映射Class,注意是从_getObjc2ClassRefs函数中取出类的引用
Class *classrefs = _getObjc2ClassRefs(hi, &count);
for (i = 0; i < count; i++) {
remapClassRef(&classrefs[i]);
}
// 重映射父类
classrefs = _getObjc2SuperRefs(hi, &count);
for (i = 0; i < count; i++) {
remapClassRef(&classrefs[i]);
}
}
}
// 将所有SEL都注册到哈希表中,是另外一张哈希表
static size_t UnfixedSelectors;
sel_lock();
for (EACH_HEADER) {
if (hi->isPreoptimized()) continue;
bool isBundle = hi->isBundle();
SEL *sels = _getObjc2SelectorRefs(hi, &count);
UnfixedSelectors += count;
for (i = 0; i < count; i++) {
const char *name = sel_cname(sels[i]);
// 注册SEL的操作
sels[i] = sel_registerNameNoLock(name, isBundle);
}
}
// 修复旧的函数指针调用遗留
for (EACH_HEADER) {
message_ref_t *refs = _getObjc2MessageRefs(hi, &count);
if (count == 0) continue;
for (i = 0; i < count; i++) {
// 内部将常用的alloc、objc_msgSend等函数指针进行注册,并fix为新的函数指针
fixupMessageRef(refs+i);
}
}
// 遍历所有协议列表,并且将协议列表加载到Protocol的哈希表中
for (EACH_HEADER) {
extern objc_class OBJC_CLASS_$_Protocol;
// cls = Protocol类,所有协议和对象的结构体都类似,isa都对应Protocol类
Class cls = (Class)&OBJC_CLASS_$_Protocol;
assert(cls);
// 获取protocol哈希表
NXMapTable *protocol_map = protocols();
bool isPreoptimized = hi->isPreoptimized();
bool isBundle = hi->isBundle();
// 从编译器中读取并初始化Protocol
protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
for (i = 0; i < count; i++) {
readProtocol(protolist[i], cls, protocol_map,
isPreoptimized, isBundle);
}
}
// 修复协议列表引用,优化后的images可能是正确的,但是并不确定
for (EACH_HEADER) {
// 需要注意到是,下面的函数是_getObjc2ProtocolRefs,和上面的_getObjc2ProtocolList不一样
protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
for (i = 0; i < count; i++) {
remapProtocolRef(&protolist[i]);
}
}
// 实现非懒加载的类,对于load方法和静态实例变量
for (EACH_HEADER) {
classref_t *classlist =
_getObjc2NonlazyClassList(hi, &count);
for (i = 0; i < count; i++) {
Class cls = remapClass(classlist[i]);
if (!cls) continue;
// 实现所有非懒加载的类(实例化类对象的一些信息,例如rw)
realizeClass(cls);
}
}
// 遍历resolvedFutureClasses数组,实现所有懒加载的类
if (resolvedFutureClasses) {
for (i = 0; i < resolvedFutureClassCount; i++) {
// 实现懒加载的类
realizeClass(resolvedFutureClasses[i]);
resolvedFutureClasses[i]->setInstancesRequireRawIsa(false/*inherited*/);
}
free(resolvedFutureClasses);
}
// 发现和处理所有Category
for (EACH_HEADER) {
// 外部循环遍历找到当前类,查找类对应的Category数组
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
// 内部循环遍历当前类的所有Category
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
// 首先,通过其所属的类注册Category。如果这个类已经被实现,则重新构造类的方法列表。
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
// 将Category添加到对应Class的value中,value是Class对应的所有category数组
addUnattachedCategoryForClass(cat, cls, hi);
// 将Category的method、protocol、property添加到Class
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
}
// 这块和上面逻辑一样,区别在于这块是对Meta Class做操作,而上面则是对Class做操作
// 根据下面的逻辑,从代码的角度来说,是可以对原类添加Category的
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
}
}
}
// 初始化从磁盘中加载的所有类,发现Category必须是最后执行的
// 从runtime objc4-532版本源码来看,DebugNonFragileIvars字段一直是-1,所以不会进入这个方法中
if (DebugNonFragileIvars) {
realizeAllClasses();
}
#undef EACH_HEADER
}
复制代码
其内部还调用了很多其他函数,后面会详细介绍函数内部实现。
load images
在项目中经常用到 load 类方法, load 类方法的调用时机比 main 函数还要靠前。 load 方法是由系统来调用的,并且在整个程序运行期间,只会调用一次,所以可以在 load 方法中执行一些只执行一次的操作。
一般 Method Swizzling 都会放在 load 方法中执行,这样在执行 main 函数前,就可以对类方法进行交换。可以确保正式执行代码时,方法肯定是被交换过的。
如果对一个类添加Category后,并且重写其原有方法,这样会导致Category中的方法覆盖原类的方法。但是load方法却是例外,所有Category和原类的load方法都会被执行。
源码分析
load 方法由 Runtime 进行调用,下面我们分析一下 load 方法的实现, load 的实现源码都在 objc-loadmethod.mm 中。
在一个新的工程中,我们创建一个 TestObject 类,并在其 load 方法中打一个断点,看一下系统的调用堆栈。
从调用栈可以看出,是通过系统的动态链接器 dyld 开始的调用,然后调到 Runtime 的 load_images 函数中。 load_images 函数是通过 _dyld_objc_notify_register 函数,将自己的函数指针注册给 dyld 的。
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(↦_images, load_images, unmap_image);
}
复制代码
在 load_images 函数中主要做了两件事,首先通过 prepare_load_methods 函数准备 Class load list 和 Category load list ,然后通过 call_load_methods 函数调用已经准备好的两个方法列表。
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!hasLoadMethods((const headerType *)mh)) return;
prepare_load_methods((const headerType *)mh);
call_load_methods();
}
复制代码
首先我们看一下 prepare_load_methods 函数的实现,看一下其内部是怎么查找 load 方法的。可以看到,其内部主要分为两部分,查找 Class 的 load 方法列表和查找 Category 方法列表。
准备类的方法列表时,首先通过 _getObjc2NonlazyClassList 函数获取所有非懒加载类的列表,这时候获取到的是一个 classref_t 类型的数组,然后遍历数组添加 load 方法列表。
Category 过程也是类似,通过 _getObjc2NonlazyCategoryList 函数获取所有非懒加载 Category 的列表,得到一个 category_t 类型的数组,需要注意的是这是一个指向指针的指针。然后对其进行遍历并添加到 load 方法列表, Class 和 Category 的 load 方法列表是两个列表。
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
// 获取到非懒加载的类的列表
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
// 设置Class的调用列表
schedule_class_load(remapClass(classlist[i]));
}
// 获取到非懒加载的Category列表
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
// 忽略弱链接的类别
if (!cls) continue;
// 实例化所属的类
realizeClass(cls);
// 设置Category的调用列表
add_category_to_loadable_list(cat);
}
}
复制代码
在添加类的 load 方法列表时,内部会递归遍历把所有父类的 load 方法都添加进去,顺着继承者链的顺序添加,会先把父类添加在前面。然后会调用 add_class_to_loadable_list 函数,将自己的 load 方法添加到对应的数组中。
static void schedule_class_load(Class cls)
{
if (!cls) return;
// 已经添加Class的load方法到调用列表中
if (cls->data()->flags & RW_LOADED) return;
// 确保super已经被添加到load列表中,默认是整个继承者链的顺序
schedule_class_load(cls->superclass);
// 将IMP和Class添加到调用列表
add_class_to_loadable_list(cls);
// 设置Class的flags,表示已经添加Class到调用列表中
cls->setInfo(RW_LOADED);
}
复制代码
而 Category 则不需要考虑父类的问题,所以直接在 prepare_load_methods 函数中遍历 Category 数组,然后调用 add_category_to_loadable_list 函数即可。
在 add_category_to_loadable_list 函数中,会判断当前 Category 有没有实现 load 方法,如果没有则直接 return ,如果实现了则添加到 loadable_categories 数组中。
类的 add_class_to_loadable_list 函数内部实现也是类似,区别在于类的数组叫做 loadable_classes 。
void add_category_to_loadable_list(Category cat)
{
IMP method;
// 获取Category的load方法的IMP
method = _category_getLoadMethod(cat);
// 如果Category没有load方法则return
if (!method) return;
// 如果已使用大小等于数组大小,对数组进行动态扩容
if (loadable_categories_used == loadable_categories_allocated) {
loadable_categories_allocated = loadable_categories_allocated*2 + 16;
loadable_categories = (struct loadable_category *)
realloc(loadable_categories,
loadable_categories_allocated *
sizeof(struct loadable_category));
}
loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}
复制代码
到此为止, loadable_classes 和 loadable_categories 两个数组已经准备好了, load_images 会调用 call_load_methods 函数执行这些 load 方法。在这个方法中, call_class_loads 函数是负责调用类方法列表的, call_category_loads 负责调用 Category 的方法列表。
void call_load_methods(void)
{
bool more_categories;
void *pool = objc_autoreleasePoolPush();
do {
// 反复执行call_class_loads函数,直到数组中没有可执行的load方法
while (loadable_classes_used > 0) {
call_class_loads();
}
more_categories = call_category_loads();
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
复制代码
下面是调用类方法列表的代码,内部主要是通过对 loadable_classes 数组进行遍历,并获取到 loadable_class 的结构体,结构体中存在 Class 和 IMP ,然后直接调用即可。
Category 的调用方式和类的一样,就不在下面贴代码了。需要注意的是, load 方法都是直接调用的,并没有走运行时的 objc_msgSend 函数。
static void call_class_loads(void)
{
int i;
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
(*load_method)(cls, SEL_load);
}
if (classes) free(classes);
}
struct loadable_class {
Class cls; // may be nil
IMP method;
};
复制代码
根据上面的源码分析,我们可以看出 load 方法的调用顺序应该是 “父类 -> 子类 -> 分类” 的顺序。因为执行加载 Class 的时机是在 Category 之前的,而且 load 子类之前会先 load 父类,所以是这种顺序。
initialize
和 load 方法类似的也有 initialize 方法, initialize 方法也是由 Runtime 进行调用的,自己不可以直接调用。与 load 方法不同的是, initialize 方法是在第一次调用类所属的方法时,才会调用 initialize 方法,而 load 方法是在 main 函数之前就全部调用了。所以理论上来说 initialize 可能永远都不会执行,如果当前类的方法永远不被调用的话。
下面我们研究一下 initialize 在 Runtime 中的源码。
在向对象发送消息时, lookUpImpOrForward 函数中会判断当前类是否被初始化,如果没有被初始化,则先进行初始化再调用类的方法。
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver);
// ....省略好多代码
// 第一次调用当前类的话,执行initialize的代码
if (initialize && !cls->isInitialized()) {
_class_initialize (_class_getNonMetaClass(cls, inst));
}
// ....省略好多代码
复制代码
在进行初始化的时候,和 load 方法的调用顺序一样,会按照继承者链先初始化父类。 _class_initialize 函数中关键的两行代码是 callInitialize 和 lockAndFinishInitializing 的调用。
// 第一次调用类的方法,初始化类对象
void _class_initialize(Class cls)
{
Class supercls;
bool reallyInitialize = NO;
// 递归初始化父类。initizlize不用显式的调用super,因为runtime已经在内部调用了
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
_class_initialize(supercls);
}
{
monitor_locker_t lock(classInitLock);
if (!cls->isInitialized() && !cls->isInitializing()) {
cls->setInitializing();
reallyInitialize = YES;
}
}
if (reallyInitialize) {
_setThisThreadIsInitializingClass(cls);
if (MultithreadedForkChild) {
performForkChildInitialize(cls, supercls);
return;
}
@try {
// 通过objc_msgSend()函数调用initialize方法
callInitialize(cls);
}
@catch (...) {
@throw;
}
@finally {
// 执行initialize方法后,进行系统的initialize过程
lockAndFinishInitializing(cls, supercls);
}
return;
}
else if (cls->isInitializing()) {
if (_thisThreadIsInitializingClass(cls)) {
return;
} else if (!MultithreadedForkChild) {
waitForInitializeToComplete(cls);
return;
} else {
_setThisThreadIsInitializingClass(cls);
performForkChildInitialize(cls, supercls);
}
}
}
复制代码
通过 objc_msgSend 函数调用 initialize 方法。
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
复制代码
lockAndFinishInitializing 函数中会完成一些初始化操作,其内部会调用 _finishInitializing 函数,在函数内部会调用 class 的 setInitialized 函数,核心工作都由 setInitialized 函数完成。
static void lockAndFinishInitializing(Class cls, Class supercls)
{
monitor_locker_t lock(classInitLock);
if (!supercls || supercls->isInitialized()) {
_finishInitializing(cls, supercls);
} else {
_finishInitializingAfter(cls, supercls);
}
}
复制代码
负责初始化类和元类,函数内部主要是查找当前类和元类中是否定义了某些方法,然后根据查找结果设置类和元类的一些标志位。
void
objc_class::setInitialized()
{
Class metacls;
Class cls;
// 获取类和元类对象
cls = (Class)this;
metacls = cls->ISA();
bool inherited;
bool metaCustomAWZ = NO;
if (MetaclassNSObjectAWZSwizzled) {
metaCustomAWZ = YES;
inherited = NO;
}
else if (metacls == classNSObject()->ISA()) {
// 查找是否实现了alloc和allocWithZone方法
auto& methods = metacls->data()->methods;
for (auto mlists = methods.beginCategoryMethodLists(),
end = methods.endCategoryMethodLists(metacls);
mlists != end;
++mlists)
{
if (methodListImplementsAWZ(*mlists)) {
metaCustomAWZ = YES;
inherited = NO;
break;
}
}
}
else if (metacls->superclass->hasCustomAWZ()) {
metaCustomAWZ = YES;
inherited = YES;
}
else {
auto& methods = metacls->data()->methods;
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
if (methodListImplementsAWZ(*mlists)) {
metaCustomAWZ = YES;
inherited = NO;
break;
}
}
}
if (!metaCustomAWZ) metacls->setHasDefaultAWZ();
if (PrintCustomAWZ && metaCustomAWZ) metacls->printCustomAWZ(inherited);
bool clsCustomRR = NO;
if (ClassNSObjectRRSwizzled) {
clsCustomRR = YES;
inherited = NO;
}
// 查找元类是否实现MRC方法,如果是则进入if语句中
if (cls == classNSObject()) {
auto& methods = cls->data()->methods;
for (auto mlists = methods.beginCategoryMethodLists(),
end = methods.endCategoryMethodLists(cls);
mlists != end;
++mlists)
{
if (methodListImplementsRR(*mlists)) {
clsCustomRR = YES;
inherited = NO;
break;
}
}
}
else if (!cls->superclass) {
clsCustomRR = YES;
inherited = NO;
}
else if (cls->superclass->hasCustomRR()) {
clsCustomRR = YES;
inherited = YES;
}
else {
// 查找类是否实现MRC方法,如果是则进入if语句中
auto& methods = cls->data()->methods;
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
if (methodListImplementsRR(*mlists)) {
clsCustomRR = YES;
inherited = NO;
break;
}
}
}
if (!clsCustomRR) cls->setHasDefaultRR();
if (PrintCustomRR && clsCustomRR) cls->printCustomRR(inherited);
metacls->changeInfo(RW_INITIALIZED, RW_INITIALIZING);
}
复制代码
需要注意的是, initialize 方法和 load 方法不太一样, Category 中定义的 initialize 方法会覆盖原方法而不是像 load 方法一样都可以调用。
简书由于排版的问题,阅读体验并不好,布局、图片显示、代码等很多问题。所以建议到我 Github 上,下载 Runtime PDF 合集。把所有 Runtime 文章总计九篇,都写在这个 PDF 中,而且左侧有目录,方便阅读。
下载地址: Runtime PDF 麻烦各位大佬点个赞,谢谢!:grin:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
图论——一个迷人的世界
本杰明,查特兰,张萍 / 机械工业出版社 / 2001-1-1
本书介绍了图论的基本概念,解释了图论中各种经典问题。例如,熄灯的问题、小生成树问题、哥尼斯堡七桥问题、中国邮递员问题、国际象棋中马的遍历问题和路的着色问题等等。书中也给出了各种类型的图,例如,二部图、欧拉图、彼得森图和树;等等。每一章都为读者设置了练习题,包含了具有挑战性的探索性问题。一起来看看 《图论——一个迷人的世界》 这本书的介绍吧!