深入理解 Objective-C ☞ Category
栏目: Objective-C · 发布时间: 5年前
内容简介:日常开发中经常会用到 Category,对于其使用方法就不多做说明了,本篇主要介绍其底层实现原理。依然从一个例子开始,给
日常开发中经常会用到 Category,对于其使用方法就不多做说明了,本篇主要介绍其底层实现原理。
1.底层结构
1.1 编译后的结构
依然从一个例子开始,给 HHStaff
这个类创建一个分类 HHStaff+CateA
,如下所示:
// HHStaff+CateA.h @interface HHStaff (CateA) <NSCopying, NSCoding> @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSInteger num; - (void)methodA1; - (void)methodA2; + (void)classMethodA1; + (void)classMethodA2; @end // HHStaff+CateA.m @implementation HHStaff (CateA) - (void)methodA1 { NSLog(@"这是 methodA1"); } - (void)methodA2 { NSLog(@"这是 methodA2"); } + (void)classMethodA1 { NSLog(@"这是 classMethodA1"); } + (void)classMethodA2 { NSLog(@"这是 classMethodA2"); } @end
然后终端执行 $ clang -rewrite-objc HHStaff+CateA.m
编译后,我们发现了这样一个结构:
struct _category_t { const char *name; // 类名 struct _class_t *cls; // const struct _method_list_t *instance_methods; // 实例方法列表 const struct _method_list_t *class_methods; // 类方法列表 const struct _protocol_list_t *protocols; // 协议方法列表 const struct _prop_list_t *properties; // 属性列表 };
这是 Category 在内存中的基本结构,其中包括其所属主类的类名、分类中的实例方法列表、类方法列表、协议方法列表以及属性列表。我们自己写的分类 HHStaff+CateA
编译后是这样的:
static struct _category_t _OBJC_$_CATEGORY_HHStaff_$_CateA __attribute__ ((used, section ("__DATA,__objc_const"))) = { "HHStaff", 0, // &OBJC_CLASS_$_HHStaff, (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_HHStaff_$_CateA, (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_HHStaff_$_CateA, (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_HHStaff_$_CateA, (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_HHStaff_$_CateA, };
上边的 _OBJC_$_CATEGORY_HHStaff_$_CateA
就是自己添加的那个分类 HHStaff+CateA
编译后的结构,它是 struct _category_t
类型的,也就是上边提到的分类的基本结构。
下边这张图详细介绍了编译后的结构:
1.2 OC 源码中的结构
在 objc 的源码中全局搜索 category_t, 最终在 objc-runtime-new.h
中发现了它的结构,如下所示:
struct category_t { const char *name; classref_t cls; struct method_list_t *instanceMethods; struct method_list_t *classMethods; struct protocol_list_t *protocols; struct property_list_t *instanceProperties; // Fields below this point are not always present on disk. // 类属性列表 struct property_list_t *_classProperties; // 获取方法列表:对象方法或类方法 method_list_t *methodsForMeta(bool isMeta) { if (isMeta) return classMethods; else return instanceMethods; } // 获取属性列表的方法(声明):对象属性or类属性 property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi); };
上边结构中,注释行以上部分的结构与我们编译后文件中的结构体 category_t
完全一致,注释行以后部分中的 _classProperties
(类属性列表) 没有接触过,后期如果遇到了再补上 O(∩_∩)O~
2.Category 合并到 Class
从上边一节我们了解到 Category 经编译后是一个与 Class 相互独立的结构,那么其中的方法、属性等信息是如何和主类联系起来的呢?其实,这一切都是在运行时发生的。下面我就开始深入 runtime 源码来探究这个相互关联的过程。
objc 的入口函数是 objc-os.mm
中的 _objc_init()
,源码如下:
void _objc_init(void) { // 使用一个静态局部变量,避免了重复初始化 static bool initialized = false; if (initialized) return; initialized = true; // 初始化操作 environ_init(); tls_init(); static_init(); lock_init(); exception_init(); // 注册 dyld 事件的监听 _dyld_objc_notify_register(↦_images, load_images, unmap_image); }
此函数的最后一行注册了 3 个 dyld 事件的监听,分别是:
map_images() load_images() unmap_image()
这里我们看一下 map_images()
这个函数的源码:
void map_images(unsigned count, const char * const paths[], const struct mach_header * const mhdrs[]) { mutex_locker_t lock(runtimeLock); // return map_images_nolock(count, paths, mhdrs); }
这里又嵌套调用了 map_images_nolock()
这个函数,为了排除干扰,这里略去了其他代码,精简后的结构如下:
void map_images_nolock(unsigned mhCount, const char * const mhPaths[], const struct mach_header * const mhdrs[]) { // ... 此处略去 n 多字 ... if (hCount > 0) { // 读取镜像,参数:hList 即 headerList,hCount 即 headerCount。 _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses); } // ... 此处略去 n 多字 ... }
hCount 即头文件的个数,也就是说有新的头文件,才会执行后边的 _read_images()
函数,各参数的含义从名称就可以看得出来。
下面看看精简后的读取镜像的函数 _read_images()
:
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) { // ... // Discover categories. for (EACH_HEADER) { // 1.得到一个存放着分类的地址的一维数组 category_t **catlist = _getObjc2CategoryList(hi, &count); bool hasClassProperties = hi->info()->hasCategoryClassProperties(); for (i = 0; i < count; i++) { // 2.获取数组中每一个 category_t 类型的指针及 Category 所属的主类 category_t *cat = catlist[i]; // cat 是一个指向结构体 category_t 的指针 Class cls = remapClass(cat->cls); // Category 所属的主类 // ... // 3.处理 Category // 3.1 类对象 bool classExists = NO; if (cat->instanceMethods || cat->protocols || cat->instanceProperties) { addUnattachedCategoryForClass(cat, cls, hi); if (cls->isRealized()) { remethodizeClass(cls); // cls 是类对象,重新方法化,重新组织类里边的方法 classExists = YES; } // ... } // 3.2 元类对象 if (cat->classMethods || cat->protocols || (hasClassProperties && cat->_classProperties)) { addUnattachedCategoryForClass(cat, cls->ISA(), hi); if (cls->ISA()->isRealized()) { remethodizeClass(cls->ISA()); // cls->ISA() 是元类对象,重新方法化,重新组织元类里边的方法 } // ... } } // 内层 for } // 外层 for // ... }
这里有 2 层 for 循环,首先看最外边的 for 循环,其中 EACH_HEADER
这个宏的定义为:
#define EACH_HEADER (hIndex = 0; hIndex < hCount && (hi = hList[hIndex]); hIndex++)
也就是依次取出 hList 里的每一个 header ,执行后边的操作,后边的操作大概可以分为 3 项:
- 取出存放着分类地址的一维数组
catlist
; - 获取
catlist
中每一个 category_t 类型的指针cat
及 Category 所属的主类cls
; - 处理 Category,依次针对
类对象
和元类对象
重新组织其中的方法,即remethodizeClass(cls)
或remethodizeClass(cls->ISA())
。
remethodizeClass()
这个函数的作用是将 Category 中的信息添加到 class 里边,其源码如下:
// Attach outstanding categories to an existing class. static void remethodizeClass(Class cls) { category_list *cats; bool isMeta; runtimeLock.assertLocked(); isMeta = cls->isMetaClass(); // Re-methodizing: check for more categories if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) { // ... // 将分类中的方法、属性及协议添加到 class 里边 attachCategories(cls, cats, true /*flush caches*/); free(cats); } }
上边的 if 条件语句中执行了 attachCategories()
这个函数,它的作用是将分类中的方法、属性及协议添加到 class 里边,源码如下:
static void attachCategories(Class cls, category_list *cats, bool flush_caches) { if (!cats) return; if (PrintReplacedMethods) printReplacements(cls, cats); bool isMeta = cls->isMetaClass(); // fixme rearrange to remove these intermediate allocations method_list_t **mlists = (method_list_t **)malloc(cats->count * sizeof(*mlists)); // 方法列表数组(即二维数组) property_list_t **proplists = (property_list_t **)malloc(cats->count * sizeof(*proplists)); // 属性列表数组(即二维数组) protocol_list_t **protolists = (protocol_list_t **)malloc(cats->count * sizeof(*protolists)); // 协议列表数组(即二维数组) // Count backwards through cats to get newest categories first int mcount = 0; int propcount = 0; int protocount = 0; int i = cats->count; bool fromBundle = NO; // 1.整合一个类中所有分类的方法数组、属性数组和协议数组 while (i--) { auto& entry = cats->list[i]; // 取出一个分类 // 1.1 方法 method_list_t *mlist = entry.cat->methodsForMeta(isMeta); if (mlist) { mlists[mcount++] = mlist; // 一次循环就将一个分类的方法列表添加到 mlists 里边,作为一个元素。 fromBundle |= entry.hi->isBundle(); } // 1.2 属性 property_list_t *proplist = entry.cat->propertiesForMeta(isMeta, entry.hi); if (proplist) { proplists[propcount++] = proplist; // 与上边类似 } // 1.3 协议 protocol_list_t *protolist = entry.cat->protocols; if (protolist) { protolists[protocount++] = protolist; // 与上边类似 } } // 2.将上一步整合的数组添加到类结构里边 auto rw = cls->data(); // 2.1 方法 prepareMethodLists(cls, mlists, mcount, NO, fromBundle); rw->methods.attachLists(mlists, mcount); free(mlists); // 2.2 属性 if (flush_caches && mcount > 0) flushCaches(cls); rw->properties.attachLists(proplists, propcount); free(proplists); // 2.3 协议 rw->protocols.attachLists(protolists, protocount); free(protolists); }
这里主要做了 2 件事:
- 整合一个类的每一个分类的方法数组(属性数组/协议数组)到一个二维数组里边,二维数组的每一个元素就是一个分类的方法列表(属性列表/协议列表);
- 执行 class 中的 data() 方法返回一个可读可写的结构,它的类型是一个结构体
class_rw_t
,这个结构体在 上一篇 已经介绍过。然后,执行attachLists()
函数,将上一步整合的方法数组添加到class_rw_t
这个结构体里边。
需要注意的是,在整合那一步 while 循环中,条件是 i–,而 i 是分类的个数 (int i = cats->count;),也就是从后往前取,即先取后编译的分类。
现在我们来看上方代码中最后调用的核心方法 attachLists()
:
void attachLists(List* const * addedLists, uint32_t addedCount) { // addedLists 是二维数组,addedCount 是数组元素个数 [[], [], []] if (addedCount == 0) return; if (hasArray()) { // many lists -> many lists // 1.扩容 uint32_t oldCount = array()->count; uint32_t newCount = oldCount + addedCount; setArray((array_t *)realloc(array(), array_t::byteSize(newCount))); // 根据 newCount 重新分配数组 array()->count = newCount; // 2.移动老数组到末尾 memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0])); // 3.拷贝新数组到头部 memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); } else if (!list && addedCount == 1) { // 0 lists -> 1 list list = addedLists[0]; } else { // 1 list -> many lists List* oldList = list; uint32_t oldCount = oldList ? 1 : 0; uint32_t newCount = oldCount + addedCount; setArray((array_t *)malloc(array_t::byteSize(newCount))); array()->count = newCount; if (oldList) array()->lists[addedCount] = oldList; memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); } }
此方法主要做了以下三件事(原数组:类中原来的方法(属性、协议等)数组,新数组:category中整合的方法(属性、协议)数组):
- 原数组扩容
realloc()
- 移动原数组到末尾
memmove()
- 拷贝新数组到扩容后数组的头部
memcpy()
(以方法为例)
至此,分类中的方法、属性、协议就整合到了类结构里边。
根据前文的讨论,分类中的方法会在运行时和主类中的方法整合到一起,并且分类中的方法会放在前边,那么当查找方法的时候,就会优先查找分类的方法,如果分类和主类有相同的方法,主类中的方法就不会执行,也就是平时所说的 “覆盖” 主类方法,其实主类中的对应方法还是存在的,只是没机会执行而已。
3. 相关问题扩展
3.1 load
我们都知道 runtime 在加载类和分类时会调用 +load 方法,那么具体是怎么调用的,这就要从前文提到的回调函数 load_images()
开始说起了,这个方法的主要作用就是执行 load
方法。
void load_images(const char *path __unused, const struct mach_header *mh) { // Return without taking locks if there are no +load methods here. if (!hasLoadMethods((const headerType *)mh)) return; recursive_mutex_locker_t lock(loadMethodLock); // 1.Discover load methods { mutex_locker_t lock2(runtimeLock); prepare_load_methods((const headerType *)mh); } // 2.Call +load methods (without runtimeLock - re-entrant) call_load_methods(); }
这个方法主要干了 2 件事:①准备 load 方法;②执行 load 方法。下面分别介绍一下这两件事:
3.1.1 准备 load 方法
准备 load 方法的工作主要是通过下边这个函数 prepare_load_methods()
执行的:
void prepare_load_methods(const headerType *mhdr) { size_t count, i; runtimeLock.assertLocked(); classref_t *classlist = _getObjc2NonlazyClassList(mhdr, &count); for (i = 0; i < count; i++) { // 1.组织类里边的 load 方法 schedule_class_load(remapClass(classlist[i])); } 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; // category for ignored weak-linked class realizeClass(cls); assert(cls->ISA()->isRealized()); // 2.将分类添加到 loadable_list add_category_to_loadable_list(cat); } }
上方函数总共做了两件事:①组织类里边的 load 方法;②将分类添加到 loadable_list,现在分别看看具体干了什么。
3.1.1.1 组织类里边的 load 方法
static void schedule_class_load(Class cls) { if (!cls) return; assert(cls->isRealized()); // _read_images should realize if (cls->data()->flags & RW_LOADED) return; // 1.递归调用当前函数 schedule_class_load(cls->superclass); // 2.将含有 load 方法的类及对应的 load 方法存储到 loadable_list 里边 add_class_to_loadable_list(cls); cls->setInfo(RW_LOADED); }
这里先做了一次递归调用,然后将传入的 cls
加到 loadable_list
里边,这样做的效果是:先将父类加进 loadable_list
,再加子类。
那么具体是怎么添加的呢,下面的源码将告诉我们真相:
void add_class_to_loadable_list(Class cls) { IMP method; loadMethodLock.assertLocked(); // 1.获取 load 方法,如果 cls 没有 load 方法,就不往下执行了 method = cls->getLoadMethod(); if (!method) return; // ... if (loadable_classes_used == loadable_classes_allocated) { loadable_classes_allocated = loadable_classes_allocated*2 + 16; loadable_classes = (struct loadable_class *)realloc(loadable_classes, loadable_classes_allocated *sizeof(struct loadable_class)); } // 2.保存 cls 和 method 至 loadable_classes loadable_classes[loadable_classes_used].cls = cls; loadable_classes[loadable_classes_used].method = method; loadable_classes_used++; // loadable_class 的数量自加,遍历执行 load 方法时会用到 }
这里也做了两件事:
- 查找 load 方法,源码如下,可以看出来,是通过比较字符串的方法找 load 方法的。
IMP objc_class::getLoadMethod() { runtimeLock.assertLocked(); const method_list_t *mlist; assert(isRealized()); assert(ISA()->isRealized()); assert(!isMetaClass()); assert(ISA()->isMetaClass()); // 便利方法列表,查找 load 方法 mlist = ISA()->data()->ro->baseMethods(); if (mlist) { for (const auto& meth : *mlist) { const char *name = sel_cname(meth.name); // 比较字符串 if (0 == strcmp(name, "load")) { return meth.imp; } } } return nil; }
- 将 load 方法和对应的 class 组合成
loadable_class
存储到loadable_classes
里边。其中loadable_classes
是个一维数组,loadable_class
是一个结构体,它的定义如下,
struct loadable_class { Class cls; // may be nil IMP method; };
3.1.1.2 将分类添加到 loadable_list
添加分类至 loadable_list 是通过 add_category_to_loadable_list()
函数实现的,源码如下:
void add_category_to_loadable_list(Category cat) { IMP method; loadMethodLock.assertLocked(); // 1.获取分类中的 load 方法,如果没有,就不往下执行 method = _category_getLoadMethod(cat); 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)); } // 2.保存分类和 load 方法至 loadable_categories 里边 loadable_categories[loadable_categories_used].cat = cat; loadable_categories[loadable_categories_used].method = method; loadable_categories_used++; }
基本原理与上边的 add_class_to_loadable_list()
相同,区别在于这里是添加到了 loadable_categories
里边,并且被添加的是 loadable_category
,定义如下。
struct loadable_category { Category cat; // may be nil IMP method; };
3.1.2 执行 load 方法
前边已经准备好了 load 方法,现在就该调用了,也就是执行下边的 call_load_methods()
函数。
void call_load_methods(void) { static bool loading = NO; bool more_categories; loadMethodLock.assertLocked(); // Re-entrant calls do nothing; the outermost call will finish the job. if (loading) return; loading = YES; void *pool = objc_autoreleasePoolPush(); // *** 调用 load 方法 do { // 1.调用所有类的 +load 方法 while (loadable_classes_used > 0) { call_class_loads(); } // 2.调用所有分类的 +load 方法 more_categories = call_category_loads(); } while (loadable_classes_used > 0 || more_categories); objc_autoreleasePoolPop(pool); loading = NO; }
此方法也是做了两件事: 先
调用所有 类
的 load 方法, 后
调用所有 分类
的 load 方法。
3.1.2.1 调用类的 load 方法
下边是执行类的 load 方法的函数 call_class_loads()
,主要是遍历 loadable_classes 类里边的每一个 loadable_class,取出其中存储的函数指针,直接去调用函数。也就是说,没有走 objc_msgSend() 的流程,而是直接通过函数地址调用函数,那么也就不存在 “覆盖” 的问题。
static void call_class_loads(void) { int i; // 1.拿到 loadable_classes 及元素个数 loadable_classes_used struct loadable_class *classes = loadable_classes; int used = loadable_classes_used; loadable_classes = nil; loadable_classes_allocated = 0; loadable_classes_used = 0; // 2.遍历 loadable_classes,调用每一个元素的 load 方法 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); } // Destroy the detached list. if (classes) free(classes); }
3.1.2.2 调用分类的 load 方法
调用分类的 load 方法的操作都在下面的 call_category_loads()
函数里边。
static bool call_category_loads(void) { int i, shift; bool new_categories_added = NO; // Detach current loadable list. struct loadable_category *cats = loadable_categories; int used = loadable_categories_used; int allocated = loadable_categories_allocated; loadable_categories = nil; loadable_categories_allocated = 0; loadable_categories_used = 0; // Call all +loads for the detached list. for (i = 0; i < used; i++) { Category cat = cats[i].cat; load_method_t load_method = (load_method_t)cats[i].method; Class cls; if (!cat) continue; cls = _category_getClass(cat); if (cls && cls->isLoadable()) { // ... (*load_method)(cls, SEL_load); cats[i].cat = nil; } } // ... return new_categories_added; }
仔细观察上边的函数,就会发现它和 call_class_loads()
的逻辑基本一致,这里就不多做说明了。
通过上边的讨论我们可以得出调用 +load
方法的顺序:
+load
根据编译顺序调用(先编译,先调用)
调用子类的 +load
之前会先调用父类的 +load
② 调用分类的 +load
根据编译顺序调用(先编译,先调用)
3.2 initialize
众所周知, +initialize
会在类第一次接收到消息时调用,那么它到底是怎么调用的呢?
我们可以先按照这样的思路来考虑,OC 的方法调用经编译后都会变成这样的函数调用 objc_msgSend(object, @selector(method))
,于是可以推断 objc_msgSend()
会调用 +initialize
,而且应该做了判断,如果调用过了一次,就不再重复调用。但是搜索 objc 源码后发现 objc_msgSend()
是用汇编实现的 (⊙﹏⊙)b,看着有点累。
另外,我们知道执行方法时,会先根据 isa 指针找到对应的类对象或元类对象,然后查找需要的方法。objc 的源码中有 class_getInstanceMethod()
和 class_getClassMethod()
2 个函数,根据字面意思判断应该是获取方法的函数,那么
+initialize
应该也在这里调用,现在就以 class_getInstanceMethod()
为例,验证一下我们的推断。
下面是函数 class_getInstanceMethod()
的源码:
Method class_getInstanceMethod(Class cls, SEL sel) { if (!cls || !sel) return nil; #warning fixme build and search caches // Search method lists, try method resolver, etc. lookUpImpOrNil(cls, sel, nil, NO/*initialize*/, NO/*cache*/, YES/*resolver*/); #warning fixme build and search caches return _class_getMethod(cls, sel); }
注意到上边的函数中有一个搜索方法列表的函数 lookUpImpOrNil()
,其实现如下(做了适当精简):
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) { // ... // 1.优先查找缓存 if (cache) { imp = cache_getImp(cls, sel); if (imp) return imp; } runtimeLock.lock(); checkIsKnownClass(cls); if (!cls->isRealized()) { realizeClass(cls); } // 2.执行 initialize 方法 if (initialize && !cls->isInitialized()) { runtimeLock.unlock(); _class_initialize (_class_getNonMetaClass(cls, inst)); // 重点 runtimeLock.lock(); } // 3.后边是查找方法步骤,这里就不罗列了... }
该方法主要做了下面几件事:
1.优先查找缓存 (Optimistic cache lookup)
2.如果需要执行+initialize
,并且当前类没有执行过
+initialize
,就去执行
+initialize
3.后边是查找方法步骤:①查缓存;②查方法列表;③查父类的缓存和方法列表
第 2 件事执行 +initialize
方法时,调用的是 _class_initialize()
这个函数,源码如下,同样做了适当精简:
void _class_initialize(Class cls) { assert(!cls->isMetaClass()); Class supercls; bool reallyInitialize = NO; // 1.递归调用父类的 +initialize supercls = cls->superclass; if (supercls && !supercls->isInitialized()) { _class_initialize(supercls); } // Try to atomically set CLS_INITIALIZING. { monitor_locker_t lock(classInitLock); if (!cls->isInitialized() && !cls->isInitializing()) { cls->setInitializing(); reallyInitialize = YES; } } if (reallyInitialize) { // Record that we're initializing this class so we can message it. _setThisThreadIsInitializingClass(cls); // ... #if __OBJC2__ @try #endif { // 2.调用 +initialize callInitialize(cls); if (PrintInitializing) { _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]", pthread_self(), cls->nameForLogging()); } } // ... return; } else if (cls->isInitializing()) { // ... } else if (cls->isInitialized()) { // ... } else { // ... } // ... }
这段代码说明类的 +initialize
方法的执行顺序是,先递归调用父类的 +initialize
,然后调用自己的 +initialize
,最终的执行如下所示:
void callInitialize(Class cls) { ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize); asm(""); }
上边最终执行的是 objc_msgSend()
这个函数,也就是说还是会根据 isa 和 super 查找方法,也就存在下边这种特殊情况了。
Note that +initialize is sent to the superclass (again) if this class doesn’t implement +initialize. 2157218
如果当前类没有实现 +initialize 方法,那么就会再调用一次父类的 +initialize 方法
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 【1】JavaScript 基础深入——数据类型深入理解与总结
- 深入理解java虚拟机(1) -- 理解HotSpot内存区域
- 深入理解 HTTPS
- 深入理解 HTTPS
- 深入理解 SecurityConfigurer
- 深入理解 HTTP 协议
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
地理信息系统算法基础
张宏、温永宁、刘爱利/国别:中国大陆 / 科学出版社 / 2006-6 / 35.00元
《地理信息系统算法基础》全面、系统地收集和整理了当前地理信息系统算法领域的相关资料,以地理信息系统设计与实现为线索,内容涉及地理空间数据的描述、检索、存储和管理,以及地理空间信息分析基本方法的设计和实现。《地理信息系统算法基础》可作为地理信息系统专业的本科生和研究生教材,也可作为从事地理信息系统软件开发和应用的人员的学习资料,并可供地理信息系统的理论研究人员参考。一起来看看 《地理信息系统算法基础》 这本书的介绍吧!