Objective-C Runtime

Objective-C Runtime 是什么?

RunTime简称运行时。就是系统在运行的时候的一些机制,其中最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数( C语言的函数调用请看这里 )。编译完成之后直接顺序执行,无任何二义性。OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编 译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。

那OC是怎么实现动态调用的呢?下面我们来看看OC通过发送消息来达到动态调用的秘密。假如在OC中写了这样的一个代码:

1
[obj makeText];

其中obj是一个对象,makeText是一个函数名称。对于这样一个简单的调用。在编译时RunTime会将上述代码转化成

1
objc_msgSend(obj,@selector(makeText));

首先我们来看看obj这个对象,iOS中的obj都继承于NSObject。

1
2
3
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

在NSObjcet中存在一个Class的isa指针。然后我们看看Class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct objc_class *Class;
struct objc_class {
  Class isa; // 指向metaclass
   
  Class super_class ; // 指向其父类
  const char *name ; // 类名
  long version ; // 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取
  long info; // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
  long instance_size ; // 该类的实例变量大小(包括从父类继承下来的实例变量);
  struct objc_ivar_list *ivars; // 用于存储每个成员变量的地址
  struct objc_method_list **methodLists ; // 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法;
  struct objc_cache *cache; // 指向最近使用的方法的指针,用于提升效率;
  struct objc_protocol_list *protocols; // 存储该类遵守的协议
    }


我们可以看到,对于一个Class类中,存在很多东西,下面我来一一解释一下:

Class isa:指向metaclass,也就是静态的Class。一般一个Obj对象中的isa会指向普通的Class,这个Class中存储普通成员变量和对 象方法(“-”开头的方法),普通Class中的isa指针指向静态Class,静态Class中存储static类型成员变量和类方法(“+”开头的方 法)。

Class super_class:指向父类,如果这个类是根类,则为NULL。

下面一张图片很好的描述了类和对象的继承关系:

iuqQFnm.png

注意:所有metaclass中isa指针都指向跟metaclass。而跟metaclass则指向自身。Root metaclass是通过继承Root class产生的。与root class结构体成员一致,也就是前面提到的结构。不同的是Root metaclass的isa指针指向自身。

Class类中其他的成员这里就先不做过多解释了,下面我们来看看:

@selector (makeText):这是一个SEL方法选择器。SEL其主要作用是快速的通过方法名字(makeText)查找到对应方法的函数指针,然后调用其函 数。SEL其本身是一个Int类型的一个地址,地址中存放着方法的名字。对于一个类中。每一个方法对应着一个SEL。所以iOS类中不能存在2个名称相同 的方法,即使参数类型不同,因为SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。

下面我们就来看看具体消息发送之后是怎么来动态查找对应的方法的。

首先,编译器将代码[obj makeText];转化为objc_msgSend(obj, @selector (makeText));,在objc_msgSend函数中。首先通过obj的isa指针找到obj对应的class。在Class中先去cache中 通过SEL查找对应函数method(猜测cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度),若 cache中未找到。再去methodList中查找,若methodlist(类的分发表)中未找到,则取superClass中查找。若能找到,则将method加 入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。





Objective-C 的 Runtime 是一个运行时库(Runtime Library),它是一个主要使用 C 和汇编写的库,为 C 添加了面相对象的能力并创造了 Objective-C。这就是说它在类信息(Class information) 中被加载,完成所有的方法分发,方法转发,等等。Objective-C runtime 创建了所有需要的结构体,让 Objective-C 的面相对象编程变为可能。

Objective-C Runtime 术语

更深入之前,咱们先了解点术语。Mac 和 iPhone 开发者关心的有两个 runtime:Modern Runtime(现代的 Runtime) 和 Legacy Runtime(过时的 Runtime)。Modern Runtime:覆盖所有 64 位的 Mac OS X 应用和所有 iPhone OS 的应用。 Legacy Runtime: 覆盖其他的所有应用(所有 32 位的 Mac OS X 应用) Method 有 2 种基本类型的方法。Instance Method(实例方法):以 ‘-’ 开始,比如 -(void)doFoo; 在对象实例上操作。Class Method(类方法):以 ‘+’ 开始,比如 +(id)alloc。方法(Methods)和 C 的函数很像,是一组代码,执行一个小的任务,如:

1
2
3
4
- (NSString *)movieTitle
{
     return  @ "Futurama: Into the Wild Green Yonder" ;
}

Selector 在 Objective-C 中 selector 只是一个 C 的数据结构,用于表示一个你想在一个对象上执行的 Objective-C 方法。在 runtime 中的定义像这样…

1
typedef struct objc_selector  *SEL;

像这样使用…

1
SEL aSel = @selector(movieTitle);

Message(消息)

1
[target getMovieTitleForObject:obj];

消息是方括号 ‘[]’ 中的那部分,由你要向其发送消息的对象(target),你想要在上面执行的方法(method)还有你发送的参数(arguments)组成。 Objective-C 的消息和 C 函数调用是不同的。事实上,你向一个对象发送消息并不意味着它会执行它。Object(对象)会检查消息的发送者,基于这点再决定是执行一个不同的方法还是转发消息到另一个目标对象上。Class 如果你查看一个类的runtime信息,你会看到这个…

1
2
3
4
typedef struct objc_class *Class;
typedef struct objc_object {
     Class isa;
} *id;

这里有几个事情。我们有一个 Objective-C 类的结构体和一个对象的结构体。objc_object 只有一个指向类的 isa 指针,就是我们说的术语 “isa pointer”(isa 指针)。这个 isa 指针是当你向对象发送消息时,Objective-C Runtime 检查一个对象并且查看它的类是什么然后开始查看它是否响应这些 selectors 所需要的一切。最后我么看到了 id 指针。默认情况下 id 指针除了告诉我们它们是 Objective-C 对象外没有其他用了。当你有一个 id 指针,然后你就可以问这个对象是什么类的,看看它是否响应一个方法,等等,然后你就可以在知道这个指针指向的是什么对象后执行更多的操作了。你可以在 LLVM/Clang 的文档中的 Block 中看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Block_literal_1 {
     void *isa;  // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock    
     int flags;    
     int reserved;     
     void (*invoke)(void *, ...); 
     struct Block_descriptor_1 { 
         unsigned long int reserved;  // NULL     
         unsigned long int size;   // sizeof(struct Block_literal_1)
         // optional helper functions     
         void (*copy_helper)(void *dst, void *src);
         void (*dispose_helper)(void *src);     
     } *descriptor;    
     // imported variables
};

Blocks 被设计为兼容 Objective-C 的 runtime,所以他们被作为对象对待,因此他们可以响应消息,比如 -retain,-release,-copy ,等等。IMP(方法实现 Method Implementations)

1
typedef id (*IMP)(id self,SEL _cmd,...);

IMP 是指向方法实现的函数指针,由编译器为你生成。如果你新接触 Objective-C 你现在不需要直接接触这些,但是我们将会看到,Objective-C  runtime 将如何调用你的方法的。Objective-C Classes(Objective-C 类) 那么什么是 Objective-C 类?在 Objective-C 中的一个类实现看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface MyClass : NSObject {
     // vars
     NSInteger counter;
}
// methods
-(void)doFoo;
@end
 
但是 runtime 不只要追踪这些
 
#if !__OBJC2__   
     Class super_class                        OBJC2_UNAVAILABLE;
     const char *name                         OBJC2_UNAVAILABLE;
     long version                             OBJC2_UNAVAILABLE;
     long info                                OBJC2_UNAVAILABLE;
     long instance_size                       OBJC2_UNAVAILABLE;    
     struct objc_ivar_list *ivars             OBJC2_UNAVAILABLE;    
     struct objc_method_list **methodLists    OBJC2_UNAVAILABLE;    
     struct objc_cache *cache                 OBJC2_UNAVAILABLE;    
     struct objc_protocol_*protocols          OBJC2_UNAVAILABLE;
#endif

我们可以看到,一个类有其父类的引用,它的名字,实例变量,方法,缓存还有它遵循的协议。runtime 在响应类或实例的方法时需要这些信息。

那么 Class 定义的是对象还是对象本身?它是如何实现的 (译注:读者需要区分 Class 和 class 是不同的,正如 Nil 和 nil 的用途是不同的)

是的,之前我说过 Objective-C 类也是对象,runtime 通过创建 Meta Classes 来处理这些。当你发送一个消息像这样 [NSObject alloc] 你正在向类对象发送一个消息,这个类对象需要是 MetaClass 的实例,MetaClass 也是 root meta class 的实例。当你说继承自 NSObject 时,你的类指向 NSObject 作为自己的 superclass。然而,所有的 meta class 指向 root metaclass 作为自己的 superclass。所有的 meta class 只是简单的有一个自己响应的方法列表。所以当你向一个类对象发送消息如 [NSObject alloc],然后实际上 objc_msgSend() 会检查 meta class 看看它是否响应这个方法,如果他找到了一个方法,就在这个 Class 对象上执行(译注:class 是一个实例对象的类型,Class 是一个类(class)的类型。对于完全的 OO 来说,类也是个对象,类是类类型(MetaClass)的实例,所以类的类型描述就是 meta class)。

为什么我们继承自苹果的类

从你开始 Cocoa 开发时,那些教程就说如继承自 NSObject 然后开始写一些代码,你享受了很多继承自苹果的类所带来的便利。有一件事你从未意识到的是你的对象被设置为使用 Objective-C 的 runtime。当我们为我们的类的一个实例分配了内存,像这样…

1
MyObject *object = [[MyObject alloc] init];

最先执行的消息是 +alloc。如果你查看下文档, 它说“新的实例对象的 isa 实例变量被初始化为指向一个数据结构,那个数据结构描述了这个类;其他的实例变量被初始化为 0。”所以继承自苹果的类不仅仅是继承了一些重要的属性,也继承了能在内存中轻松分配内存的能力和在内存中创建满足 runtime 期望的对象结构(设置 isa 指针指向我们的类)。

那么 Class Cache 是什么?(objc_cache *cache)

当 Objective-C runtime 沿着一个对象的 isa 指针检查时,它会发现一个对象实现了许多的方法。然而你可能只调用其中一小部分的方法,也没有意义每次检查时搜索这个类的分发表(dispatch table)中的所有 selector。所以这个类实现了一个缓存,当你搜索一个类的分发表,并找到合适的 selector 后,就会把它放进缓存中。所以当 objc_msgSend() 在一个类中查找 selector 时会先查找类缓存。有个理论是,当你在一个类上调用了一个消息,你很可能之后还会调用它。所以如果我们考虑到这点,就意味着当我们有个子类继承自 NSObject 叫做 MyObject 并且运行了以下的代码

1
2
3
4
5
6
7
8
9
10
MyObject *obj = [[MyObject alloc] init]; 
 
@implementation MyObject
- (id)init {
     if (self = [ super  init]) {
         [self setVarA:@”blah”];    
     }
     return  self;
}
@end

发生了以下的事:

(1) [MyObject alloc] 首先被执行。MyObject 没有实现 alloc 方法,所以我们不能在这个类中找到 +alloc 方法,然后沿着 superclass 指针会指向 NSObject。

(2) 我们询问 NSObject 是否响应 +alloc 方法,它可以。+alloc 检查消息的接收者类,是 MyObject,然后分配一块和我们的类同样大小的内存空间,并初始化它的 isa 指针指向 MyObject 类,我们现在有了一个实例对象,最终把类对象的 +alloc 方法加入 NSObject 的类缓存(class cache)中(lastly we put +alloc in NSObject's class cache for the class object )。

(3) 到现在为止,我们发送了一个类消息,但是现在我们发送一个实例消息,只是简单的调用 -init 或者我们设计的初始化方法。当然,我们的类会响应这个方法,所以 -(id)init 加入到缓存中。(译注:要是 MyObject 实现了 init 方法,就会把 init 方法加入到 MyObject 的 class cache 中,要是没有实现,只是因为继承才有了这个方法,init 方法还是会加入到 NSObject 的 class cache 中)。

(4) 然后 self = [super init] 被调用。super 是个 magic keyword,指向对象的父类,所以我们得到了 NSObject 并调用它的的 init 方法。这样可以确保 OOP(面相对象编程) 的继承功能正常,这个方法可以正确的初始化父类的变量,之后你(在子类中)可以初始化自己的变量,如果需要可以覆盖父类的方法。在 NSObject 的例子中,没什么重要的要做,但并不总是这样。有时要做些重要的初始化。比如…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#import  
 
@interface MyObject : NSObject {
     NSString *aString;
     @property(retain) NSString *aString; 
 
@end 
 
@implementation MyObject
 
-(id)init 
     if  (self = [ super  init]) {
         [self setAString:nil];
     }
     return  self;
}
 
@synthesize aString;
 
@end
 
int main (int argc, const char * argv[]) 
{
     NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
 
     id obj1 = [NSMutableArray alloc]; 
     id obj2 = [[NSMutableArray alloc] init];   
     id obj3 = [NSArray alloc]; 
     id obj4 = [[NSArray alloc] initWithObjects:@ "Hello" ,nil];
 
     NSLog(@ "obj1 class is %@" ,NSStringFromClass([obj1 class]));
     NSLog(@ "obj2 class is %@" ,NSStringFromClass([obj2 class]));
     NSLog(@ "obj3 class is %@" ,NSStringFromClass([obj3 class]));
     NSLog(@ "obj4 class is %@" ,NSStringFromClass([obj4 class]));
 
     id obj5 = [MyObject alloc]; 
     id obj6 = [[MyObject alloc] init];   
 
     NSLog(@ "obj5 class is %@" ,NSStringFromClass([obj5 class]));    
     NSLog(@ "obj6 class is %@" ,NSStringFromClass([obj6 class]));   
 
     [pool drain];
     return  0;
}

现在如果你新接触 Cocoa ,我让你猜会会输出什么,你可能会说

1
2
3
4
5
6
NSMutableArray
NSMutableArray 
NSArray
NSArray
MyObject
MyObject

但是,实际上是

1
2
3
4
5
6
obj1 class is __NSPlaceholderArray
obj2 class is NSCFArray
obj3 class is __NSPlaceholderArray
obj4 class is NSCFArray
obj5 class is MyObject
obj6 class is MyObject

这是因为在 Objective-C 中 +alloc 方法可能会返回某个类的对象,然后在 -init 中返回另一个类的对象。
(译注:感兴趣的同学可以看下这两篇文章:Class ClustersMake Your Own Abstract Factory Class Cluster in Objective-C, 第二篇文章需要自备小梯子。)

那么在 objc_msgSend 中发生了什么?

事实上在 objc_msgSend() 中发生了许多事儿。假设我们有这样的代码…

1
[self printMessageWithString:@ "Hello World!" ];

它实际上会被编译器翻译为…

1
objc_msgSend(self,@selector(printMessageWithString:),@ "Hello World!" );

我们沿着目标对象的 isa 指针查找,看看是否这个对象响应 @selector(printMessageWithString:) selector。假设我们在类的分发表或者缓存中找到了这个 selector,我们沿着函数指针并且执行它。这样 objcmsgSend() 就永远不会返回,它开始执行,然后沿着指向方法的指针,然后你的方法返回,这样看起来 objcmsgSend() 方法返回了。Bill Bumgarner 比我讲了更多 objc_msgSend() 的细节(部分1部分2 和 部分3)。

概括下他说的,并且你已经看过了 Objective-C 的 runtime 代码…

检查忽略的 Selector 和短路(Short Circut)—— 显然,如果我们运行在垃圾回收机制下,我们可以忽略调用 -retain, -release, 等等。

检查 nil 对象(target)。和其他的语言不一样的是,在 Objective-C 中向 nil 发送消息是完全合法的,并且有些原因下你会愿意这么做的。假设我们有个非 nil 的对象,然后我们继续…

然后我们需要在这个类上找到 IMP,所以我们先从 class cache 中找起,如果找到了就沿着指针跳到这个函数。

如果没有在缓存中找到 IMP,然后去查找类的分发表,如果找到了,就沿着指针跳到这个函数。

如果 IMP 没有在缓存和类的分发表中找到,然后我们跳到转发机制。这意味着最终你的代码被编译器转换为 C 函数。你写的方法会像这样…

1
-(int)doComputeWithNum:(int)aNum

会被翻译为…

1
int aClass_doComputeWithNum(aClass *self,SEL _cmd,int aNum)

Objective-C Runtime 通过调用(invoking)指向这些方法的函数指针调用你的方法(call your methods)。现在,我要说的是,你不能直接调用这些被翻译的方法,但是 Cocoa 框架提供了获得函数指针的方法…

1
2
3
4
5
6
7
8
9
//declare C function pointer
int (computeNum *)(id,SEL,int);
 
//methodForSelector is COCOA & not ObjC Runtime
//gets the same function pointer objc_msgSend gets
computeNum = (int (*)(id,SEL,int))[target methodForSelector:@selector(    doComputeWithNum:)]; 
 
//execute the C function pointer returned by the runtime
computeNum(obj,@selector(doComputeWithNum:),aNum);

通过这种方法,你可以直接访问这个函数,并且可以在运行时直接调用,甚至可以使用这个避开 runtime 的动态特性,如果你绝对需要确保一个方法被执行。Objective-C 就是用这种途径去调用你的方法的,但是使用的是 objc_msgSend()。

Objective-C 消息转发

在 Objective-C 中向一个不知道如何响应这个方法的对象发送消息是完全合法的(甚至可能是一种潜在的设计决定)。苹果的文档中给出的一个原因是模拟多继 承,Objective-C 不是原生支持的,或者你可能只是想抽象你的设计并且隐藏幕后处理这些消息的其他对象/类。这一点是 runtime 非常需要的。它是这样做的 1. Runtime 检查了你的类和所有父类的 class cache 和分发表,但是没找到指定的方法。2. Objective_C 的 Runtime  会在你的类上调用 + (BOOL) resolveInstanceMethod:(SEL)aSEL。 这就给了你一个机会去提供一个方法实现并且告诉 runtime 你已经解析了这个方法,如果它开始查找,这回就会找到这个方法。你可以像这样实现…定义一个函数…

1
2
3
4
void fooMethod(id obj, SEL _cmd)
     NSLog(@ "Doing Foo" );
}

然后你可以像这样使用 class_addMethod() 解析它…

1
2
3
4
5
6
7
8
9
+(BOOL)resolveInstanceMethod:(SEL)aSEL
{
     if (aSEL == @selector(doFoo:))
     {
             class_addMethod([self class],aSEL,(IMP)fooMethod, "v@:" );
             return  YES;
     }
     return  [ super  resolveInstanceMethod];
}

在 class_addMethod() 最后一部分的 "v@:" 是方法的返回和参数类型。你可以在 Runtime Guide 的 Type Encoding 章节看到完整介绍。 3. Runtime 然后调用 – (id)forwardingTargetForSelector:(SEL)aSelector。这样做是为了给你一次机会(因为我们不能解析这个方法 (参见上面的 #2))引导 Objective-C runtime 到另一个可以响应这个消息的对象上,在花费昂贵的处理过程调用  – (void)forwardInvocation:(NSInvocation *)anInvocation 之前调用这个方法也是更好的。你可以像这样实现

1
2
3
4
5
6
7
8
- (id)forwardingTargetForSelector:(SEL)aSelector
{
     if (aSelector == @selector(mysteriousMethod:))
     {        
         return  alternateObject;
     }
     return  [ super  forwardingTargetForSelector:aSelector];
}

显然你不想从这个方法直接返回 self,否则可能会产生一个死循环。 4. Runtime 最后一次会尝试在目标对象上调用 – (void)forwardInvocation:(NSInvocation *)anInvocation。如果你从没看过 NSInvocation,它是 Objective-C 消息的对象形式。一旦你有了一个 NSInvocation 你可以改变这个消息的一切,包括目标对象,selector 和参数。所以你可以这样做…  

1
2
3
4
5
6
7
8
9
-(void)forwardInvocation:(NSInvocation *)invocation
{  
     SEL invSEL = invocation.selector;    
     if ([altObject respondsToSelector:invSEL]) {        
         [invocation invokeWithTarget:altObject];    
     else  {        
         [self doesNotRecognizeSelector:invSEL];    
     }
}

如果你继承自 NSObject,默认它的 – (void)forwardInvocation:(NSInvocation *)anInvocation 实现只是简单的调用 -doesNotRecognizeSelector:,你可以在最后一次机会里覆盖这个方法去做一些事情。(译注:对这块内容有兴趣的同学可以参见:http://www.cnblogs.com/biosli/p/NSObjectinherit2.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值