参考文章

1. `文/滕先洪(简书作者)原文链接:http://www.jianshu.com/p/ab966e8a82e2著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”[下载地址]        (https://github.com/XHTeng/XHRuntimeDemo)`2.`http://www.code4app.com/forum.php?mod=viewthread&tid=8241&highlight=runtime`

什么是runtime

runtime 是 OC底层的一套C语言的API(引入<objc/runtime.h> 或<objc/message.h> ),编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:clang -rewrite-objc xxx.m可以看到编译后的xxx.cpp(C++文件)。

  • RunTime简称运行时,就是系统在运行的时候的一些机制,其中最主要的是消息机制。
  • 对于C语言,函数的调用在编译的时候会决定调用哪个函数,编译完成之后直接顺序执行,无任何二义性。
  • OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编 译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。
  • 只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。
  • 我们写的oc代码,它在运行的时候也是转换成了runtime方式运行的,更好的理解runtime,也能帮我们更深的掌握oc语言。
  • 每一个oc的方法,底层必然有一个与之对应的runtime方法。
  • 当我们用OC写下这样一段代码[tableView cellForRowAtIndexPath:indexPath];
  • 在编译时RunTime会将上述代码转化成[发送消息]objc_msgSend(tableView, @selector(cellForRowAtIndexPath,indexPath);

runtime的作用

  1. 动态交换两个方法的实现(特别是交换系统自带的方法);
  2. 动态添加对象的成员变量和成员方法;
  3. 获得某个类的所有成员方法、所有成员变量;

具体一点就是

  1. 获得某个类的所有成员方法、所有成员变量;
  2. 拦截系统自带的方法调用(Swizzle 黑魔法),比如拦截imageNamed:、viewDidLoad、alloc;
  3. 实现分类也可以增加属性;
  4. 实现NSCoding的自动归档和自动解档;
  5. 实现字典和模型的自动转换。

案例汇总

案例一:方法简单的交换

需要用到的方法 <objc/runtime.h>

  • 获得某个类的类方法
    Method class_getClassMethod(Class cls , SEL name)
  • 获得某个类的实例对象方法
    Method class_getInstanceMethod(Class cls , SEL name)
  • 交换两个方法的实现
    void method_exchangeImplementations(Method m1 , Method m2)

创建一个Person类,类中实现以下两个类方法,并在.h 文件中声明

+ (void)run {
NSLog(@"跑");
}+ (void)study {
NSLog(@"学习");
}

调用方法,并通过runtime实现方法交换

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{person *aperson = [[person alloc]init];[person run];
[person study];// 获取两个类的类方法
Method m1 = class_getClassMethod([person class], @selector(run));
Method m2 = class_getClassMethod([person class], @selector(study));
// 开始交换方法实现
method_exchangeImplementations(m1, m2);
// 交换后,先打印学习,再打印跑!
[person run];
[person study];}

打印结果为:

2016-07-11 14:10:28.033 runtime demo[37610:2393684] 跑
2016-07-11 14:10:28.034 runtime demo[37610:2393684] 学习
2016-07-11 14:10:28.034 runtime demo[37610:2393684] 学习
2016-07-11 14:10:28.034 runtime demo[37610:2393684] 跑

案例二:拦截并替换方法

需求:比如iOS6 升级 iOS7 后需要版本适配,根据不同系统使用不同样式图片(拟物化和扁平化),如何通过不去手动一个个修改每个UIImage的imageNamed:方法就可 以实现为该方法中加入版本判断语句?

步骤:
1、为UIImage建一个分类(UIImage+Category)
2、在分类中实现一个自定义方法,方法中写要在系统方法中加入的语句,比如版本判断

+ (UIImage *)xh_imageNamed:(NSString *)name {
double version = [[UIDevice currentDevice].systemVersion doubleValue];
if (version >= 7.0) {// 如果系统版本是7.0以上,使用另外一套文件名结尾是‘_os7’的扁平化图片name = [name stringByAppendingString:@"_os7"];
}
return [UIImage xh_imageNamed:name];
}

3、分类中重写UIImage的load方法,实现方法的交换(只要能让其执行一次方法交换语句,load再合适不过了)

+ (void)load {
// 获取两个类的类方法
Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
Method m2 = class_getClassMethod([UIImage class], @selector(xh_imageNamed:));
// 开始交换方法实现
method_exchangeImplementations(m1, m2);
}

注意:自定义方法中最后一定要再调用一下系统的方法,让其有加载图片的功能,但是由于方法交换,系统的方法名已经变成了我们自定义的方法名(有点绕,就是用我们的名字能调用系统的方法,用系统的名字能调用我们的方法),这就实现了系统方法的拦截!

利用以上思路,我们还可以给 NSObject 添加分类,统计创建了多少个对象,给控制器添加分类,统计有创建了多少个控制器,特别是公司需求总变的时候,在一些原有控件或模块上添加一个功能,建议使用该方法!

案例三:在分类中设置属性,给任何一个对象设置属性

众所周知,分类中是无法设置属性的,如果在分类的声明中写@property 只能为其生成get 和 set 方法的声明,但无法生成成员变量,就是虽然点语法能调用出来,但程序执行后会crash,有人会想到使用全局变量呢?比如这样:

int _age;- (int )age {return _age;
}- (void)setAge:(int)age {_age = age;
}

但是全局变量程序整个执行过程中内存中只有一份,我们创建多个对象修改其属性值都会修改同一个变量,这样就无法保证像属性一样每个对象都拥有其自己的属性值。这时我们就需要借助runtime为分类增加属性的功能了。

需要用到的方法 <objc/runtime.h>

  • set方法,将值value 跟对象object 关联起来(将值value 存储到对象object 中)
    参数 object:给哪个对象设置属性
    参数 key:一个属性对应一个Key,将来可以通过key取出这个存储的值,key 可以是任何类型:double、int 等,建议用char 可以节省字节
    参数 value:给属性设置的值
    参数policy:存储策略 (assign 、copy 、 retain就是strong)
    void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy)
  • 利用参数key 将对象object中存储的对应值取出来
    id objc_getAssociatedObject(id object , const void *key)

步骤:
1、创建一个分类,比如给任何一个对象都添加一个name属性,就是NSObject添加分类(NSObject+Category)

2、先在.h 中@property 声明出get 和 set 方法,方便点语法调用

@property(nonatomic,copy)NSString *name;

3、在.m 中重写set 和 get 方法,内部利用runtime 给属性赋值和取值

char nameKey;- (void)setName:(NSString *)name {// 将某个值跟某个对象关联起来,将某个值存储到某个对象中objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}- (NSString *)name {return objc_getAssociatedObject(self, &nameKey);
}

案例四:获得一个类的所有成员变量

最典型的用法就是一个对象在归档和解档的 encodeWithCoder和initWithCoder:方法中需要该对象所有的属性进行decodeObjectForKey: 和 encodeObject:,通过runtime我们声明中无论写多少个属性,都不需要再修改实现中的代码了。

需要用到的方法 <objc/runtime.h>

  • 获得某个类的所有成员变量(outCount 会返回成员变量的总数)
    参数:
    1、哪个类
    2、放一个接收值的地址,用来存放属性的个数
    3、返回值:存放所有获取到的属性,通过下面两个方法可以调出名字和类型
Ivar *class_copyIvarList(Class cls , unsigned int *outCount)
  • 获得成员变量的名字
const char *ivar_getName(Ivar v)
  • 获得成员变量的类型
const char *ivar_getTypeEndcoding(Ivar v)

获取Person类中所有成员变量的名字和类型

unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([Person class], &outCount);// 遍历所有成员变量
for (int i = 0; i < outCount; i++) {// 取出i位置对应的成员变量Ivar ivar = ivars[i];const char *name = ivar_getName(ivar);const char *type = ivar_getTypeEncoding(ivar);NSLog(@"成员变量名:%s 成员变量类型:%s",name,type);
}
// 注意释放内存!
free(ivars);

利用runtime 获取所有属性来重写归档解档方法

如果你实现过自定义模型数据持久化的过程,那么你也肯定明白,如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍encodeObject 和 decodeObjectForKey方法,如果这样的模型又有很多个,这还真的是一个十分麻烦的事情。下面来看看简单的实现方式。
假设现在有一个Movie类,有3个属性,它的h文件这这样的

#import <Foundation/Foundation.h>//1. 如果想要当前类可以实现归档与反归档,需要遵守一个协议NSCoding
@interface Movie : NSObject<NSCoding>@property (nonatomic, copy) NSString *movieId;
@property (nonatomic, copy) NSString *movieName;
@property (nonatomic, copy) NSString *pic_url;@end

如果是正常写法, m文件应该是这样的:

#import "Movie.h"
@implementation Movie- (void)encodeWithCoder:(NSCoder *)aCoder
{[aCoder encodeObject:_movieId forKey:@"id"];[aCoder encodeObject:_movieName forKey:@"name"];[aCoder encodeObject:_pic_url forKey:@"url"];}- (id)initWithCoder:(NSCoder *)aDecoder
{if (self = [super init]) {self.movieId = [aDecoder decodeObjectForKey:@"id"];self.movieName = [aDecoder decodeObjectForKey:@"name"];self.pic_url = [aDecoder decodeObjectForKey:@"url"];}return self;
}
@end

如果这里有100个属性,那么我们也只能把100个属性都给写一遍。
不过你会使用runtime后,这里就有更简便的方法。
下面看看runtime的实现方式:

#import "Movie.h"
#import <objc/runtime.h>
@implementation Movie- (void)encodeWithCoder:(NSCoder *)encoder{unsigned int count = 0;Ivar *ivars = class_copyIvarList([Movie class], &count);for (int i = 0; i<count; i++) {// 取出i位置对应的成员变量Ivar ivar = ivars;// 查看成员变量const char *name = ivar_getName(ivar);// 归档NSString *key = [NSString stringWithUTF8String:name];id value = [self valueForKey:key];[encoder encodeObject:value forKey:key];}free(ivars);
}- (id)initWithCoder:(NSCoder *)decoder
{if (self = [super init]) {unsigned int count = 0;Ivar *ivars = class_copyIvarList([Movie class], &count);for (int i = 0; i<count; i++) {// 取出i位置对应的成员变量Ivar ivar = ivars;// 查看成员变量const char *name = ivar_getName(ivar);// 归档NSString *key = [NSString stringWithUTF8String:name];id value = [decoder decodeObjectForKey:key];// 设置到成员变量身上[self setValue:value forKey:key];}free(ivars);} return self;
}
@end

这样的方式实现,不管有多少个属性,写这几行代码就搞定了。怎么,还嫌麻烦,下面看看更加简便的方法:两句代码搞定。
我们把encodeWithCoder 和 initWithCoder这两个方法抽成宏

#import "Movie.h"
#import <objc/runtime.h>#define encodeRuntime(A) unsigned int count = 0;
Ivar *ivars = class_copyIvarList([A class], &count);
for (int i = 0; i<count; i++) {
Ivar ivar = ivars;
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[encoder encodeObject:value forKey:key];
}
free(ivars);#define initCoderRuntime(A) if (self = [super init]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([A class], &count);
for (int i = 0; i<count; i++) {
Ivar ivar = ivars;
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
id value = [decoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivars);
}
return self;@implementation Movie- (void)encodeWithCoder:(NSCoder *)encoder{encodeRuntime(Movie)
}- (id)initWithCoder:(NSCoder *)decoder
{initCoderRuntime(Movie)
}
@end

我们可以把这两个宏单独放到一个文件里面,这里以后需要进行数据持久化的模型都可以直接使用这两个宏。

案例五:利用runtime 获取所有属性来进行字典转模型

以往我们都是利用KVC进行字典转模型,但是它还是有一定的局限性,例如:模型属性和键值对对应不上会crash(虽然可以重写setValue:forUndefinedKey:方法防止报错),模型属性是一个对象或者数组时不好处理等问题,所以无论是效率还是功能上,利用runtime进行字典转模型都是比较好的选择。

字典转模型我们需要考虑三种特殊情况:
1.当字典的key和模型的属性匹配不上
2.模型中嵌套模型(模型属性是另外一个模型对象)
3.数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)

字典转模型的应用可以说是每个app必然会使用的场景,虽然实现的方式略有不同,但是原理都是一致的:遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。
像几个出名的开源库:JSONModel,MJExtension等都是通过这种方式实现的。

  • 先实现最外层的属性转换

    // 创建对应模型对象
    id objc = [[self alloc] init];

    unsigned int count = 0;// 1.获取成员属性数组
    Ivar *ivarList = class_copyIvarList(self, &count);// 2.遍历所有的成员属性名,一个一个去字典中取出对应的value给模型属性赋值
    for (int i = 0; i < count; i++) {// 2.1 获取成员属性Ivar ivar = ivarList;// 2.2 获取成员属性名 C -> OC 字符串NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];// 2.3 _成员属性名 => 字典keyNSString *key = [ivarName substringFromIndex:1];// 2.4 去字典中取出对应value给模型属性赋值id value = dict[key];// 获取成员属性类型NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];}

如果模型比较简单,只有NSString,NSNumber等,这样就可以搞定了。但是如果模型含有NSArray,或者NSDictionary等,那么我们还需要进行第二步转换。

  • 内层数组,字典的转换

    if ([value isKindOfClass:[NSDictionary class]] && ![ivarType containsString:@"NS"]) { //  是字典对象,并且属性名对应类型是自定义类型// 处理类型字符串 @\"User\" -> UserivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];// 自定义对象,并且值是字典// value:user字典 -> User模型// 获取模型(user)类对象Class modalClass = NSClassFromString(ivarType);// 字典转模型if (modalClass) {// 字典转模型 uservalue = [modalClass objectWithDict:value];}}if ([value isKindOfClass:[NSArray class]]) {// 判断对应类有没有实现字典数组转模型数组的协议if ([self respondsToSelector:@selector(arrayContainModelClass)]) {// 转换成id类型,就能调用任何对象的方法id idSelf = self;// 获取数组中字典对应的模型NSString *type =  [idSelf arrayContainModelClass][key];// 生成模型Class classModel = NSClassFromString(type);NSMutableArray *arrM = [NSMutableArray array];// 遍历字典数组,生成模型数组for (NSDictionary *dict in value) {// 字典转模型id model =  [classModel objectWithDict:dict];[arrM addObject:model];}// 把模型数组赋值给valuevalue = arrM;}}

我自己觉得系统自带的KVC模式字典转模型就挺好的,假设movie是一个模型对象,dict 是一个需要转化的 [movie setValuesForKeysWithDictionary:dict]; 这个是系统自带的字典转模型方法,个人感觉也还是挺好用的,不过使用这个方法的时候需要在模型里面再实现一个方法才行,
- (void)setValue: (id)value forUndefinedKey: (NSString *)key

重写这个方法为了实现两个目的:

  1. 模型中的属性和字典中的key不一致的情况,比如字典中有个id,我们需要把它赋值给uid属性;
  2. 字典中属性比模型的属性还多的情况。如果出现以上两种情况而没有实现这个方法的话,程序就会崩溃。

这个方法的实现:

- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
if ([key isEqualToString:@"id"]) {self.uid = value;
}
}

案例六:动态变量控制

在程序中,xiaoming的age是10,后来被runtime变成了20,来看看runtime是怎么做到的。

  • 1.动态获取XiaoMing类中的所有属性[当然包括私有]

      `Ivar *ivar = class_copyIvarList([self.xiaoming class], &count);`  
  • 2.遍历属性找到对应name字段

      `const char *varName = ivar_getName(var);`
  • 3.修改对应的字段值成20

      `object_setIvar(self.xiaoMing, var, @"20");`  
  • 4.代码参考

    -(void)answer{unsigned int count = 0;Ivar ivar = class_copyIvarList([self.xiaoMing class], &count);for (int i = 0; i<count; i++) {
    Ivar var = ivar[i];
    const char varName = ivar_getName(var);
    NSString *name = [NSString stringWithUTF8String:varName];
    if ([name isEqualToString:@”_age”]) {
    object_setIvar(self.xiaoMing, var, @”20”);
    break;
    }
    }
    NSLog(@”XiaoMing’s age is %@”,self.xiaoMing.age);
    }

案例七:动态添加方法

在程序当中,假设XiaoMing的中没有guess这个方法,后来被Runtime添加一个名字叫guess的方法,最终再调用guess方法做出相应。那么,Runtime是如何做到的呢?

1.动态给XiaoMing类中添加guess方法:

class_addMethod([self.xiaoMing class], @selector(guess), (IMP)guessAnswer, "v@: ");

这里参数地方说明一下:

(IMP)guessAnswer 意思是guessAnswer的地址指针;
"v@:" 意思是,v代表无返回值void,如果是i则代表int;@代表 id sel; : 代表 SEL _cmd;
“v@:@@” 意思是,两个参数的没有返回值。

2.调用guess方法响应事件:

[self.xiaoMing performSelectorselector(guess)];

3.编写guessAnswer的实现:

void guessAnswer(id self,SEL _cmd){NSLog(@"i am from beijing");} 

这个有两个地方留意一下:

  • void的前面没有+、-号,因为只是C的代码。
  • 必须有两个指定参数(id self,SEL _cmd)。

代码参考:

-(void)answer{
class_addMethod([self.xiaoMing class], @selector(guess), (IMP)guessAnswer, "v@:");if ([self.xiaoMing respondsToSelector:@selector(guess)]) {[self.xiaoMing performSelector:@selector(guess)];} else{NSLog(@"Sorry,I don't know");}}void guessAnswer(id self,SEL _cmd){NSLog(@"i am from beijing");}

案例八:在方法上增加额外功能

有这样一个场景,出于某些需求,我们需要跟踪记录APP中按钮的点击次数和频率等数据,怎么解决?当然通过继承按钮类或者通过类别实现是一个办法,但是带来其他问题比如别人不一定会去实例化你写的子类,或者其他类别也实现了点击方法导致不确定会调用哪一个,runtime可以这样解决:

@implementation UIButton (Hook)+ (void)load {static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{Class selfClass = [self class];SEL oriSEL = @selector(sendAction:to:forEvent:);Method oriMethod = class_getInstanceMethod(selfClass, oriSEL);SEL cusSEL = @selector(mySendAction:to:forEvent:);Method cusMethod = class_getInstanceMethod(selfClass, cusSEL);BOOL addSucc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));if (addSucc) {class_replaceMethod(selfClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));}else {method_exchangeImplementations(oriMethod, cusMethod);}});}- (void)mySendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
[CountTool addClickCount];
[self mySendAction:action to:target forEvent:event];
}@end

load方法会在类第一次加载的时候被调用,调用的时间比较靠前,适合在这个方法里做方法交换,方法交换应该被保证,在程序中只会执行一次。

转载于:https://www.cnblogs.com/zhuyaguang/p/5660247.html

Runtime 总结相关推荐

  1. Go 运行时(go runtime)的含义

    go 运行时,也称为 go runtime.其本身就是每个 go 程序的一部分,它会跟你的源码一起编译并连接到目标程序中.即便你只是写了一个 hello world 程序,这个程序中也包含了 runt ...

  2. Deep Learning部署TVM Golang运行时Runtime

    Deep Learning部署TVM Golang运行时Runtime 介绍 TVM是一个开放式深度学习编译器堆栈,用于编译从不同框架到CPU,GPU或专用加速器的各种深度学习模型.TVM支持来自Te ...

  3. 使用Runtime执行推理(C++)

    使用Runtime执行推理(C++) 概述 通过MindSpore Lite模型转换后,需在Runtime中完成模型的推理执行流程.本教程介绍如何使用C++接口编写推理代码. Runtime总体使用流 ...

  4. “ compiler-rt”运行时runtime库

    " compiler-rt"运行时runtime库 编译器-rt项目包括: • Builtins-一个简单的库,提供了代码生成和其他运行时runtime组件所需的特定于目标的低级接 ...

  5. runtime系统的Cello

    runtime系统的Cello 通过充当一个现代的.功能强大的runtime系统,Cello使许多以前在C中不切实际或笨拙的事情变得简单,例如: 通用数据结构 多态函数 接口/类型类 构造函数/析构函 ...

  6. 编译器设计-RunTime运行时环境

    编译器设计-RunTime运行时环境 Compiler Design - Run-Time Environment 作为源代码的程序仅仅是文本(代码.语句等)的集合,要使其活动,它需要在目标计算机上执 ...

  7. CUDA运行时 Runtime(四)

    CUDA运行时 Runtime(四) 一. 图 图为CUDA中的工作提交提供了一种新的模型.图是一系列操作,如内核启动,由依赖项连接,依赖项与执行分开定义.这允许定义一次图形,然后重复启动.将图的定义 ...

  8. CUDA运行时Runtime(三)

    CUDA运行时Runtime(三) 一.异步并发执行 CUDA将以下操作公开为可以彼此并发操作的独立任务: 主机计算: 设备计算: 从主机到设备的内存传输: 从设备到主机的存储器传输: 在给定设备的存 ...

  9. CUDA运行时 Runtime(二)

    CUDA运行时 Runtime(二) 一. 概述 下面的代码示例是利用共享内存的矩阵乘法的实现.在这个实现中,每个线程块负责计算C的一个方子矩阵C sub,块内的每个线程负责计算Csub的一个元素.如 ...

  10. CUDA运行时 Runtime(一)

    CUDA运行时 Runtime(一) 一. 概述 运行时在cudart库中实现,该库通过静态方式链接到应用程序库cudart.lib和 libcudart.a,或动态通过cudart.dll或者lib ...

最新文章

  1. JS 中 this 的指向
  2. 文献记录(part59)--多任务学习
  3. 在微型计算机中1 mb准确等于几个字,2010安徽省计算机等级考试二级试题及答案...
  4. 海龟交易法则04_像海龟一样思考
  5. 业内最大规模多标签图像数据集开源 | GitHub资源
  6. 实验1-6 输出带框文字 (5 分)
  7. YII composer全局安装
  8. decent compiled words
  9. 十个非常实用的PS画笔使用技巧
  10. W10注册表及其打开方法
  11. 易到要在网约车市场突围并不容易
  12. 智齿科技推首款智慧客服产品:机器人代替人工
  13. 自动控制原理->控制系统性能
  14. sns.pairplot()用法
  15. OCLint + Infer + Jenkins + SonarQube 搭建iOS代码静态分析系统
  16. IComponent2 Interface 学习
  17. ctf:kali2:Legion漏洞分析
  18. android x86启动卡死,[更新]Android-x86启动成功,但是还有点小问题。
  19. 基于SuperMap Idesktop 进行白膜拉伸的方式
  20. Nova Tek Hdmiout 小板调试总结

热门文章

  1. SAP FICO hande汉得培训资料---II 应收模块篇 PDF 电子版
  2. linux学习(3) 关机使命
  3. OracleJDBC
  4. Windows server 2003下Oracle10g安装图解
  5. 关于VC向导生成的COM的注册与反注册
  6. 算法小记 · 字符串翻转
  7. spring boot + spring cloud 基础架构设计
  8. memcache的介绍与应用场景
  9. php preg_replace 正则替换图片路径
  10. 国家“十三五”重点出版规划获批