• iOS 之 runtime --- 集百家之言


    runtime

    runtime用在什么地方?

    1. 说法

      1. 在程序运行过程中,动态的创建一个类(比如KVO的底层实现)

      2. 在程序运行过程中,动态地为某个类添加属性、方法,修改属性值方法(method swizzing)

      3. 遍历一个类的所有成员变量(属性)方法

        例如:我们需要对一个类的属性进行归档的时候,属性特别多,我们就会写很多对应的代码,但是如果使用了runtime就可以动态的设置

         objc_msgSend : 给对象发送消息
         
         class_copyMethodList : 遍历某个类所有的方法
         
         class_copyIvarList : 遍历某个类所有的成员变量
         
         ...
        
    2. Runtime库主要做下面几件事:

      1. 封装

         在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。
        
      2. 找出方法的最终执行代码:

         当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething),runtime会根据消息接收者是否能响应该消息而做出不同的反应。
        

    runtime 的基础数据类型

    • Class:

        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; // 存储该类遵守的协议
        }
      
    • objc_objctid

      objc_object是表示一个类的实例的结构体,它的定义如下(objc/objc.h):

        struct objc_object {
        
            Class isa ;
        };
        
        typedef struct objc_object *id;
      

      当我们向一个Objective-C对象发送消息时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类的方法列表及父类的方法列表中去寻找与消息对应的selector指向的方法。找到后即运行这个方法。

      另外还有我们常见的id,它是一个objc_object结构类型的指针。它的存在可以让我们实现类似于C++中泛型的一些操作。该类型的对象可以转换为任何一种对象,有点类似于C语言中void *指针类型的作用。

    • objc_cache

      objc_class结构体中的cache字段,它用于缓存调用过的方法。这个字段是一个指向objc_cache结构体的指针,其定义如下:

        struct objc_cache {
        
            unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
        
            unsigned int occupied                                    OBJC2_UNAVAILABLE;
        
            Method buckets[1]                                        OBJC2_UNAVAILABLE;
        
        };
      

      该结构体的字段描述如下:

      1. mask:一个整数,指定分配的缓存bucket的总数。在方法查找过程中,Objective-C runtime使用这个字段来确定开始线性查找数组的索引位置。指向方法selector的指针与该字段做一个AND位操作(index = (mask & selector))。这可以作为一个简单的hash散列算法。

      2. occupied:一个整数,指定实际占用的缓存bucket的总数。

      3. buckets:指向Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。

    • Meta Class 元类

      在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。如:

        NSArray *array = [NSArray array];
      

      这个例子中,+array消息发送给了NSArray类,而这个NSArray也是一个对象。既然是对象,那么它也是一个objc_object指针,它包含一个指向其类的一个isa指针。那么这些就有一个问题了,这个isa指针指向什么呢?为了调用+array方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念

        meta-class是一个类对象的类。
      

      当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。

      再深入一下,meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。这样就形成了一个完美的闭环。

      对于NSObject继承体系来说,其实例方法对体系中的所有实例、类和meta-class都是有效的;而类方法对于体系内的所有类和meta-class都是有效的。

      类相关的操作函数

        // 获取类的类名
        const char * class_getName ( Class cls );
        
        // 获取类的父类
        Class class_getSuperclass ( Class cls );
      
        // 判断给定的Class是否是一个元类
        BOOL class_isMetaClass ( Class cls );
        
        // 获取实例大小
        size_t class_getInstanceSize ( Class cls );
        
        // 获取类中指定名称实例成员变量的信息
        Ivar class_getInstanceVariable ( Class cls, const char *name );
      
        // 获取类成员变量的信息
        Ivar class_getClassVariable ( Class cls, const char *name );
      
        // 添加成员变量
        BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types );
        
        
        // 获取指定的属性
        objc_property_t class_getProperty ( Class cls, const char *name );
        
        
        // 获取属性列表
        objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount );
        
        
        // 为类添加属性
        BOOL class_addProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
        
        
        // 替换类的属性
        void class_replaceProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
        
        
        // 获取整个成员变量列表
        Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );
        
        
        // ====================
        
        // 添加协议
        BOOL class_addProtocol ( Class cls, Protocol *protocol );
        
        
        // 返回类是否实现指定的协议
        BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );
        
        
        // 返回类实现的协议列表
        Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );
      

      还有一些就不在这里列举了。

    • SEL:

      表示一个方法的selector的指针,其定义如下:

        typedef struct objc_selector *SEL;
      

      objc_selector结构体的详细定义没有在头文件中找到。方法selector用于表示运行时的方法米昂子。OC在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整形标识(Int类型地址),这个标志就是SEL。如下代码所示:

        SEL sell = @selector(method1);
        NSLog("sel: %p", sell);
        // 输出: 2014-10-30 18:40:07.518 RuntimeTest[52734:466626] sel : 0x100002d72
      

      两个类之间,不管他们是父类与子类的关系,还是之间没有任何关系,只要方法名相同,那么方法的SEL就是一样的。每一个方法都对应着一个SEL。所以在OC同一个类(类的继承体系中),不能存在2个同名的方法,即使参数类型不同也不行。相同的方法只能对应一个SEL。这也就导致OC在处理相同方法名且参数个数相同但类型不同的方法方面的能力很差。

        - (void)setWidth:(int)width;
        - (void)setWidth:(double)width;
      

      当然不同类可以拥有相同的selector。 不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector区寻找自己对应的IMP.

      工程中的所有的SEL组成一个Set集合,Set的特点就是唯一,因此SEL是唯一的。因此,如果我们想到这个方法集合中查找某个方法时,只需要去 找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度 上无语伦比!!但是,有一个问题,就是数量增多会增大hash冲突而导致的性能下降(或是没有冲突,因为也可能用的是perfect hash)。但是不管使用什么样的方法加速,如果能够将总量减少(多个方法可能对应同一个SEL),那将是最犀利的方法。那么,我们就不难理解,为什么 SEL仅仅是函数名了。

      本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度.

      下面是三种方法来获取SEL:

        1. sel_registerName函数
        
        2. Objective-C编译器提供的@selector()
        
        3. NSSelectorFromString()方法
      
    • IMP

      实际上是一个函数指针,指向方法实现的首地址:

        id(*IMP)(id , SEL, ...)
      

      这个函数使用当前CPU架构实现的标准的C调用约定。第一个参数是指向self的指针(如果是实例方法,则是实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector), 第三个是参数列表

      前面介绍过的SEL就是为了查找方法的最终实现IMP的。 由于每个方法对应一个唯一的SEL,因此我们可以通过SEL方便快速准确的获得它所对应的IMP,查找过程将在下面讨论。取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以想调用普通的C语言函数一样来使用这个函数指针了。

      通过取得IMP,我们可以跳过Runtime的消息传递机制,直接执行IMP指向的函数实现,这样省去了runtime消息传递过程中所作的一些列查找操作,会比直接向对象发送消息高效一些。

    • Mehtod

        typedef struct objc_method *Method;
        
        struct objc_method{
        	SEL method_name; // 方法名
        	char *method_types; // 
        	IMP method_imp;  // 方法实现
        }
      

      我们可以看到改结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间做了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。

      ** objc_method_description**

        struct objc_method_description{SEL name, char *types;};
      

      方法相关的操作函数

        // 调用指定方法的实现
        id method_invoke ( id receiver, Method m, ... );
         
        // 调用返回一个数据结构的方法的实现
        void method_invoke_stret ( id receiver, Method m, ... );
         
        // 获取方法名
        SEL method_getName ( Method m );
         
        // 返回方法的实现
        IMP method_getImplementation ( Method m );
         
        // 获取描述方法参数和返回值类型的字符串
        const char * method_getTypeEncoding ( Method m );
         
        // 获取方法的返回值类型的字符串
        char * method_copyReturnType ( Method m );
         
        // 获取方法的指定位置参数的类型字符串
        char * method_copyArgumentType ( Method m, unsigned int index );
         
        // 通过引用返回方法的返回值类型字符串
        void method_getReturnType ( Method m, char *dst, size_t dst_len );
         
        // 返回方法的参数的个数
        unsigned int method_getNumberOfArguments ( Method m );
         
        // 通过引用返回方法指定位置参数的类型字符串
        void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
         
        // 返回指定方法的方法描述结构体
        struct objc_method_description * method_getDescription ( Method m );
         
        // 设置方法的实现
        IMP method_setImplementation ( Method m, IMP imp );
         
        // 交换两个方法的实现
        void method_exchangeImplementations ( Method m1, Method m2 );
      

      方法选择器

        // 返回给定选择器指定的方法的名称
        const char * sel_getName ( SEL sel );
         
        // 在Objective-C Runtime系统中注册一个方法,将方法名映射到一个选择器,并返回这个选择器
        SEL sel_registerName ( const char *str );
         
        // 在Objective-C Runtime系统中注册一个方法
        SEL sel_getUid ( const char *str );
         
        // 比较两个选择器
        BOOL sel_isEqual ( SEL lhs, SEL rhs );
      

      方法的调用流程

      在OC中,消息知道运行时才绑定到方法实现上。 编译器会将消息表达式转化为一个消息函数的调用,即objc_msgSend. 这个函数将消息接收者和方法名作为其基础参数,如下所示:

        obj_msgSend(receiver, selector, arg1, ...)
      

      这个函数完成了动态绑定的所有事情:

      1. 首先它找到selector对应方法的实现,因为同一个妇女饭可能在不同的类中有不同的实现,所以我们需要依赖于接收者的类来找到的确切的实现。
      2. 它调用方法实现,并将接收者对象及方法的所有参数传给它。
      3. 最后,它将实现返回值作为他自己的返回值。

      消息的关键在于:

      1. isa
      2. methodlists

      当我们创建一个新对象时,先为其分配内存,并初始化其成员变量。其中isa指针也会被初始化,让对象可以访问类及类的继承体系。

      获取方法地址

      Runtime中方法的动态绑定让我们写代码时更具灵活性,如果我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。不过灵活性的提升也带来了性能上的一些损耗。毕竟我们需要去查找方法的实现,而不像函数调用来得name直接。当然,方法的换成一定程度上解决了这一问题。

      如果想要避开这种动态绑定方式,我们可以获取方法实现的地址,然后像调用函数一样来直接调用它。特别是当我们需要在一个循环内频繁地调用一个特定的方法时,通过这种方式可以提高程序的性能。

        void (*setter)(id, SEL, BOOL);
        int i;
        setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
        for(i = 0; i < 1000; i++)
        	setter(targetList[i],@selector(setFilled:), YES);
      

      当然这种方式只适合于在类似于for循环这种情况下频繁调用同一方法,已提高性能的情况。另外,methodForSelector:是由Cocoa运行时提供的;它不是OC语言的特性。

    消息:

    1. 消息机制

       [obj makeText]
      

      其中obj是一个对象,makeText是一个函数名,对于这么一个简单的调用

       objc_msgSend(obj, @selector(makeText));
      

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

       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类型成员变量和类方法

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

      首先,编译器将代码[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中的函数指针跳转到对应的函数中去执行。

      下面我们用例子举例说明其执行过程:

       NSArray *array = [[NSArray alloc] init];
      

      其流程:

      1. [NSArray alloc]先被执行。因为NSArray没有+alloc方法,于是去父类NSObjcet中查找
      2. 检测NSObjcet时候响应+alloc,发现响应,于是检测NSArray的类,并根据其所需的内存空间大小开始分配内存空间,然后把isa指针指向NSArray类。同时,+alloc也被加进cache列表里面。
      3. 接着,执行-init方法,如果NSArray响应该方法,则直接将其加入cache;如果不响应,则去父类查找。
      4. 在后期的操作中,如果再以[[NSArray alloc] init]这种方式来创建数组,则会直接从cache中取出相应的方法,直接调用
    2. 消息转发

       if ([self respondsToSelector:@selector(method)]) {
       [self performSelector:@selector(method)];
       }
      

      消息转发机制基本上分为三个步骤:

       1. 动态方法解析:
       2. 备用接收者
       3. 完整转发
      
      1. 动态方法解析:

        对象在接收到未知消息时,首先会调用所属的类方法 +resolveInstanceMethod: 或 +resolveClassMethod:. 在这个方法中,我们有机会为该未知消息新增一个"处理方法"。 不过使用该方法的前提是我们已经实现了该处理方法.只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。

         void functionForMethod1(id self, SEL _cmd){
         	NSLog("%@ , %p", self, _cmd);
         }
         
         + (BOOL)resolveInstanceMethod:(SEL)sel{
         	
         	NSSTring *selectorString = NSStringFromSelector(sel);
         	
         	if ([selectorString isEqualToString:@"method1"]){
         		class_addMethod(self.class,@selector(method1),(IMP)functionForMethod1,"@:");
         	}
         	
         	return [super resolveInstanceMethod:sel];
         }
        

        不过这种方案更多的是为了实现@dynamic属性。

      2. 备用接收者

        如果在上一步无法处理消息,则runtime会继续调一下方法:

         - (id)forwardingTargetForSelector:(SEL)aSelector
        

        如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。

        这一步适合于我们只想将消息转发到另一个能处理改消息的对象上。但这一步无法对消息进行处理,如操作消息的参数和返回值。

      3. 完整的消息转发

         - (void)fowardInvocation:(NSInvocation *)anInvocation
        

        运行时系统会在这一步给消息接收者最后一次机会将消息转发给其他对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息 有关的全部细节都封装到anInvocation中,包括selector, 目标target和参数。我们可以在上面的方法中选择将消息转发给其他对象。

        forwardInvocation:方法的实现有两个任务:

        1. 定位可以响应封装在anInvocation中的消息对象。
        2. 使用anInvocation作为参数,将消息发送到选中的对象。anInvocation将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。

        不过,在这个方法中我们可以实现一些更复杂的功能,我们可以对消息的内容进行修改,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。

        必须重写以下方法:

         - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
        

        消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。

         - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
             NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
          
             if (!signature) {
                 if ([SUTRuntimeMethodHelper instancesRespondToSelector:aSelector]) {
                     signature = [SUTRuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
                 }
             }
          
             return signature;
         }
          
         - (void)forwardInvocation:(NSInvocation *)anInvocation {
             if ([SUTRuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
                 [anInvocation invokeWithTarget:_helper];
             }
         }	
        

        NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。

        从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。这取决于具体的实现。

        消息转发与多重继承

        回过头来看第二和第三步,通过这两个方法我们可以允许一个对象与其它对象建立关系,以处理某些未知消息,而表面上看仍然是该对象在处理消息。通过这 种关系,我们可以模拟“多重继承”的某些特性,让对象可以“继承”其它对象的特性来处理一些事情。不过,这两者间有一个重要的区别:多重继承将不同的功能 集成到一个对象中,它会让对象变得过大,涉及的东西过多;而消息转发将功能分解到独立的小的对象中,并通过某种方式将这些对象连接起来,并做相应的消息转发。

        不过消息转发虽然类似于继承,但NSObject的一些方法还是能区分两者。如respondsToSelector:和isKindOfClass:只能用于继承体系,而不能用于转发链。便如果我们想让这种消息转发看起来像是继承,则可以重写这些方法,如以下代码所示:

         	- (BOOL)respondsToSelector:(SEL)aSelector   {
         	       if ( [super respondsToSelector:aSelector] )
         	                return YES;     
         	       else {
         	                 /* Here, test whether the aSelector message can
         	                  *            
         	                  * be forwarded to another object and whether that  
         	                  *            
         	                  * object can respond to it. Return YES if it can.  
         	                  */      
         	       }
         	       return NO;  
         	}
        

    几个有关runtime的问题

    1. 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

      • 不能向编译后得到的类中增加实例变量
      • 能向运行时创建的类中添加实例变量

      解释:

      • 因为编译后类已经注册在runtime中,类结构体中 objc_ivar_list 实例变量的链表和instance_size实例变量的内存大小已经确定,同时runtime会调用class_setIvarLayoutclass_setWeakIvarLayout来处理strong weak引用。所以不能向存在的类中添加实例变量。
      • 运行时创建的类是可以添加实例变量的,调用class_addIvar函数。但是得在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上。

    ====

    最后,有一篇文章写得特别好,思路清晰:玉令天下的博客,文章不足之处是少了点KVO的内容,对于KVO也力推文章:如何自己动手实现 KVO,顺便我把我搜集的一张图贴上:招聘一个靠谱的iOS
    当然还有一些问题也可以从 《招聘一个靠谱的iOS》去了解

  • 相关阅读:
    创建授权SQL mysql数据库命令
    [mysql] 无法通过insert 创建用户ERROR 1364 (HY000): Field 'ssl_cipher' doesn't have a default value
    MySql 用户管理 中添加用户,新建数据库,用户授权,删除用户,修改密码(注意每行后边都跟个;表示一个命令语句结束):
    mysql 导出数据到文件数据异常 ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option so it cannot execute this statement
    tab切换 js制作 index和this的应用
    mysql中 where in 用法详解
    Mysql Join语法解析与性能分析
    Java 导入导出Excle表格 两种方式
    SQLyog 快捷键
    DnCNN-Beyond a Gaussian Denoiser: Residual Learning of Deep CNN for Image Denoising
  • 原文地址:https://www.cnblogs.com/Ohero/p/4875331.html
Copyright © 2020-2023  润新知