lt模式 epoll lt cpuout 什么时候会触发

起因事情的经过是这样的,有个朋友问为何swizzle addObject没有效果并贴出代码。下面是当时的实现(有修改):
+(void)load
Class class = [self class];
SEL originalSelector = @selector(addObject:);
SEL swizzledSelector = @selector(se_addObject:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
method_exchangeImplementations(originalMethod, swizzledMethod);
-(void)se_addObject:(id)anObject
NSLog(@"调用了替换后的方法");
if(anObject) {
[self se_addObject:anObject];
NSLog(@"啦啦啦啦啦");
我第一感觉se_addObject是个死循环,但是修改#1代码为[self addObject:anObject];之后发现swizzle方法仍旧没有调用。难道swizzle失败了吗?但是跟踪调试发现swizzle成功了。但是为什么没有调用呢?在解释这个问题之前,我们先NSMutableArray的继承结构:
其中以下划线开头的类是框架隐藏的类,这些继承体系可以通过class-dump来验证:
class-dump --arch i386 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation | grep ": NSArray"
Class ClustersClass Clusters()是设计模式中工厂模式的实现,这种实现类似C++中多态的实现,使用基类指针指向不同的派生类,将函数调用转发给不同派生类的实现。在这里,NSArray和NSMutableArray实际上是抽象类,通过NSArray和NSMutableArray接口创建返回的实际上是子类对象,这一过程是如何实现的呢?经过研究发现,NSArray的子类都未实现alloc函数,所以子类在调用alloc函数的时候,实际上调用的都是NSArray实现,反汇编NSArray发现allocWithZone的实现大致如下(改变为伪代码):
-(instancetype)allocWithZone:(struct _NSZone *)zone
if ([self isKindOfClass:[NSArray class]]) {
self = [__NSPlaceholderArray immutablePlaceholder];
return self;
} else if ([self isKindOfClass:[NSMutableArray class]]) {
self = [__NSPlaceholderArray mutablePlaceholder];
return self;
self = [super allocWithZone:zone];
return self;
alloc会调用allocWithZone,NSArray根据消息发送者的不同返回了不同子类。所以在我们调用[NSMutableArray alloc]的时候通过NSPlaceholderArray的类方法返回了NSPlaceholderArray类型,并保存当前数组类型信息为mutablePlaceholderArray。
id obj1 = [NSArray alloc];
id obj2 = [NSMutableArray alloc];
NSArray和NSMutableArray在作为抽象类,isa指针在初始化之前都是指向NSPlaceholderArray。所以swizzle的都是__NSPlacehodlerArray的实现。
id obj1 = [NSArray alloc];
id obj2 = [NSMutableArray alloc];
[obj1 addObject:@"aa"];
[obj2 addObject:@"aa"];
这两种情况都会调用swizzle之后的se_addObject方法。
__NSPlacehodlerArray实现如下:
- (id)init {
if (self == ___immutablePlaceholderArray()) {
self = [[__NSArrayI alloc] init];
else if (self == ___mutablePlaceholderArray()) {
self = [[__NSArrayM alloc] init];
return self;
可以通过控制台验证:
__NSPlaceholderArray
(lldb) po init] class]
__NSArrayM
__NSPlaceholderArray
(lldb) po init] class]
__NSArray0
最后那个之所以是__NSArray0,是NSPlaceholderArray在初始化时对没有元素的情况做了优化。
处理插入nil数据崩溃针对这种场景,已经有一些开源框架、。但是这样处理真的合适吗?
我觉得并不合适,原因如下:
我们的代码不能兼容错误操作。插入nil数据本身是错误操作,如果这种错误有可能出现,就尽早抛出异常,或者插入之前验证数据的合法性。对于一个库的设计来说也是如此,对用户不正确的使用要尽早抛出异常,而不是猜测用户意图,兼容错误操作。错误发现的越晚,造成的影响越大。
更改系统的实现是个危险的操作。针对当前这种情况来说,可能有其他库依赖这种特性。比如Core Data捕获了插入nil的异常并且业务里处理了这种异常,我们更改了addObject的实现之后会影响CoreData。
降低性能。作为一个包括底层框架和应用层很多库要用到的大量频繁的方法,增加一行判断也可能会降低性能。
如果真的有必要在插入数据之前判断数据合法性,可以参考苹果官方给出的,使用如下继承的方式重写相关的函数:
@interface ValidatingArray : NSMutableArray
NSMutableArray *embeddedA
References
前言众所周知,在C/C++中内存管理是非常重要的内容,内存管理是C/C++程序员的基本功,为了方便内存管理,C++的库(STL&&Boost)提供了各种智能指针的实现。同样在iOS MRC时代,iOS的开发也是需要手动内存管理,手动内存管理麻烦而且容易出错,严重影响程序健壮性和开发效率。为了改善这种情况,苹果在iOS5推出了新功能ARC(Automatic Reference Counting)。ARC是如何实现的?objc和编译器底层帮我们做了哪些事情,它们是如何管理内存的?
Autorelease在MRC时代,alloc创建的对象必须在对象生命周期结束时调用release或者在创建之初调用autorelease。如下:
NSObject *testObj = init];
//insert code
NSObject *testObj = init] autorelease];
内存管理的第一原则:谁创建,谁释放。所以方式1我们很容易理解;方式2是如何管理对象的生命周期的?其实调用autorelease方法其实是把当前对象添加到autoreleasepool中管理,我们就不需要手动释放了。
@autoreleasepooliOS工程main.m中main函数的实现如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
main函数的的业务实现我们不关心,我们只关心@autoreleasepool到底是什么,为了方便,我们改写main函数如下:
#import &Foundation/Foundation.h&
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *objTest = [[NSObject alloc]init];
在命令行中使用clang -rewrite-objc main.m让编译器用C++改写这个函数,改写后的代码简化如下:
int main(int argc, const char * argv[]) {
{ __AtAutoreleasePool __
NSObject *objTest = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
@autoreleasepool在改写后被注释掉了,同时在作用域的第一行生成了一个__AtAutoreleasePool的C++对象,全局搜索可以看到__AtAutoreleasePool定义如下:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush()
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj)
void * atautoreleasepoolobj
这个结构体(和class的唯一区别是不能定义私有变量或私有函数)只有一个构造函数和析构函数,析构函数是在作用域结束时自动调用的,所以main函数代码等价如下:
int main(int argc, const char * argv[]) {
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
objc_autoreleasePoolPop(atautoreleasepoolobj);
然后看下这两个函数的实现:
objc_autoreleasePoolPush(void)
return AutoreleasePoolPage::push();
objc_autoreleasePoolPop(void *ctxt)
AutoreleasePoolPage::pop(ctxt);
可以看出只是简单的对AutoreleasePoolPage的封装。AutoreleasePoolPage又是什么?下面就从它的结构和实现上逐步剖析AutoreleasePoolPage。
AutoreleasePoolPage的结构它的结构如下:
class AutoreleasePoolPage {
magic_t const
pthread_t const
AutoreleasePoolPage * const
AutoreleasePoolPage *
uint32_t const
magic 用来校验 AutoreleasePoolPage 的结构是否完整;
next 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() ;
thread 指向当前线程;
parent 指向父结点,第一个结点的 parent 值为 nil ;
child 指向子结点,最后一个结点的 child 值为 nil ;
depth 代表深度,从 0 开始,往后递增 1;
hiwat 代表 high water mark。
push操作objc_autoreleasePoolPush()函数实际上调用了AutoreleasePoolPage::push()函数。
static inline void *push()
dest = autoreleaseFast(POOL_BOUNDARY);
其中POOL_BOUNDARY是nil的宏定义
define POOL_BOUNDARY nil`
在首次创建AutoRelease Pool的时候插入一个nil值表示嵌套的AutoreleasePool的边界。
push函数调又autoreleaseFast实现具体的插入:
static inline id *autoreleaseFast(id obj)
AutoreleasePoolPage *page = hotPage();
if (page && !page-&full()) {
return page-&add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
return autoreleaseNoPage(obj);
插入新元素的时候分三种情况需要处理:
当前page未满,直接插入到当前page
当前page已满,创建一个新page并插入
当前page不存在,创建一个新page并插入
这三种情况最终都会调用page-&add(obj):
id *add(id obj)
assert(!full())
unprotect()
id *ret = next
*next++ = obj
return ret
首先断言当前page非满(next != end()),然后解除当前页保护(因为要插入,要使当前页可读可写),更新next指针地址,返回插入的元素地址。第一次初始化autoreleasepool时,传的nil值,返回值是nil值所在的地址。
Pop操作同上,objc_autoreleasePoolPop 实际调用了AutoreleasePoolPage::pop(ctxt),传入的参数是初始化是返回的nil值的地址:
static inline void pop(void *token)
AutoreleasePoolPage *
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
if (hotPage()) {
pop(coldPage()-&begin());
setHotPage(nil);
page = pageForPointer(token);
stop = (id *)
if (*stop != POOL_BOUNDARY) {
if (stop == page-&begin()
!page-&parent) {
return badPop(token);
if (PrintPoolHiwat) printHiwat();
page-&releaseUntil(stop);
if (page-&lessThanHalfFull()) {
page-&child-&kill();
else if (page-&child-&child) {
page-&child-&child-&kill();
pop的参数的含义是需要pop到参数元素所在的地址,Pop的时候首先判断是不是EMPTY_POOL_PLACEHOLDER, EMPTY_POOL_PLACEHOLDER的解释如下:
EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is pushed and it has never contained any objects. This saves memory when the top level (i.e. libdispatch) pushes and pops pools but never uses them.
EMPTY_POOL_PLACEHOLDER是存储在TLS中的用来表示链表最上层没有元素的pool,这样就不用创建pool可以节约内存。TLS是什么以及具体实现介绍的比较详细,这里不在解释。如果pool里面有数据,就把里面的数据清空;否者就把hotPage设置为nil。接着调用page-&releaseUntil(stop)给此参数之前的所有对象发送release释放内存,对象内存释放之后,终点page之前的page都会变成空的,最后调用page-&child-&kill()回收这些空page资源。
假设某个线程的autoreleasepool的结构如下图所示,这个autoreleasepool的堆栈里有两个POOL_SENTINEL,说明代码里嵌套了两个autoreleasepool,在整个双向链表里,起始的第一个page为coldPage,最新的最后一个是hotpage。hotPage里保存了最新添加的对象:
此时,如果执行pop(POOL_BOUNDARY)操作,那么autoreleasepool的堆栈结构会变成下图:
所以处理嵌套的autoreleasepool就很简单了,因为autoreleasepool与autoreleasepool之间用特殊的标记分割了,每个autoreleasepool的释放只需要释放到指定位置(初始化时push的nil指针的地址)即可,内层与外层互相不影响。
autorelease操作我们已经清楚了autoreleasepool的结构以及autoreleasepool对对象的管理方式,那么我们自己创建的对象是怎么和autoreleasepool关联的?在MRC中,autorelease的调用例如[[[NSObject alloc]init] autorelease]调用堆栈如下:
autorelease转发给了rootAutorelease调用:
- (id)autorelease {
return ((id)self)-&rootAutorelease();
objc_object::rootAutorelease()
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
rootAutorelease首先判断是不是Tagged Pointer,是的话直接返回,因为Tagged Pointer是栈上的对象,不需要内存管理(文末有解释)。接着用__builtin_return_address判断外部是不是ARC环境。最终调用autoreleaseFast将对象插入到autoreleasepool中。
static inline id *autoreleaseFast(id obj)
AutoreleasePoolPage *page = hotPage();
if (page && !page-&full()) {
return page-&add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
return autoreleaseNoPage(obj);
可以看出,在MRC中,通过对象主动调用autorelease的方式将对象加入到autoreleasepool的管理。那么在ARC中是怎么处理的呢?
在ARC环境下,我们改写main函数如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *testObj = [[NSObject alloc]init];
@autoreleasepool {
NSObject *objTest2 = [[NSObject alloc]init];
objTest2 = testO
然后使用clang -S -fobjc-arc -emit-llvm main.m -o main.cc生成IR中间代码,截取关键部分如下:
define i32 @main(i32, i8**) #0 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
%5 = alloca i8**, align 8
%6 = alloca %0*, align 8
%7 = alloca %1*, align 8
store i32 0, i32* %3, align 4
store i32 %0, i32* %4, align 4
store i8** %1, i8*** %5, align 8
%8 = call i8* @objc_autoreleasePoolPush()
%9 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
%10 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !7
%11 = bitcast %struct._class_t* %9 to i8*
%12 = call i8* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i8* (i8*, i8*)*)(i8* %11, i8* %10)
%13 = bitcast i8* %12 to %1*
%14 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.2, align 8, !invariant.load !7
%15 = bitcast %1* %13 to i8*
%16 = call i8* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i8* (i8*, i8*)*)(i8* %15, i8* %14)
%17 = bitcast i8* %16 to %1*
%18 = bitcast %1* %17 to %0*
store %0* %18, %0** %6, align 8
%19 = call i8* @objc_autoreleasePoolPush() #2
%20 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
%21 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !7
%22 = bitcast %struct._class_t* %20 to i8*
%23 = call i8* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i8* (i8*, i8*)*)(i8* %22, i8* %21)
%24 = bitcast i8* %23 to %1*
%25 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.2, align 8, !invariant.load !7
%26 = bitcast %1* %24 to i8*
%27 = call i8* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i8* (i8*, i8*)*)(i8* %26, i8* %25)
%28 = bitcast i8* %27 to %1*
store %1* %28, %1** %7, align 8
%29 = load %0*, %0** %6, align 8
%30 = bitcast %0* %29 to %1*
%31 = bitcast %1** %7 to i8**
%32 = bitcast %1* %30 to i8*
call void @objc_storeStrong(i8** %31, i8* %32) #2
%33 = bitcast %1** %7 to i8**
call void @objc_storeStrong(i8** %33, i8* null) #2
call void @objc_autoreleasePoolPop(i8* %19)
%34 = bitcast %0** %6 to i8**
call void @objc_storeStrong(i8** %34, i8* null) #2
call void @objc_autoreleasePoolPop(i8* %8)
可以看出在ARC环境下,在autoreleasePoolPop之前会自动为我们生成objc_storeStrong,第一个参数是当前对象,第二个是赋值的对象(=右边的)。对象在外部被引用时,就retain:
当对象没有被引用时,直接释放资源:
这里比较奇怪的是对象并没有被加入到autoreleasepool中,应该是编译器对简单的情况作了优化,毕竟autoreleasepool的管理也是一个不小的开销。
@autoreleasepool使用场景什么情况下我们需要显示使用Autorelease Pool?可以参考下苹果文档。
Each thread in a Cocoa application maintains its own stack of autorelease pool blocks. If you are writing a Foundation-only program or if you detach a thread, you need to create your own autorelease pool block.
If your application or thread is long-lived and potentially generates a lot of autoreleased objects, you should use autorelease pool blocks (like AppKit and UIKit do on the main thread); otherwise, autoreleased objects accumulate and your memory footprint grows. If your detached thread does not make Cocoa calls, you do not need to use an autorelease pool block.
需要程序员自己使用@autoreleasepool
If you are writing a program that is not based on aUI framework, such as a command-line tool.
If you write a loop that creates many temporary objects.You may use an autorelease pool block inside the loop to dispose of those objects before the next iteration. Using an autorelease pool block in the loop helps to reduce the maximum memory footprint of the application.
If you spawn a secondary thread.You must create your own autorelease pool block as soon as the thr otherwise, your application will leak objects. (See Autorelease Pool Blocks and Threads for details.)
大致意思就是大部分时候都不需要程序员关心,但是以下情况需要注意
代码不是基于UI比如基于命令行
创建大量的临时对象或者大对象需要立即释放的时候
创建了子线程时,子线程里应该使用@autoreleasepool;如从NSThread中detach出的线程用了Cocoa calls以上两种情况需要使用。
以下是几种使用的例子:
@autoreleasepool {
UIImage *diskImage = [self diskImageForKey:key];
[NSThread detachNewThreadSelector:@selector(launchExecution) toTarget:self withObject:nil];
- (void)launchExecution {
@autoreleasepool {
RLMSchema *cachedRealmS
@autoreleasepool {
cachedRealmSchema = RLMGetAnyCachedRealmForPath(config.path).
if (cachedRealmSchema) {
RLMRealmSetSchemaAndAlign(realm, cachedRealmSchema);
else if (dynamic) {
RLMRealmSetSchemaAndAlign(realm, [RLMSchema dynamicSchemaFromObjectStoreSchema:realm-&_realm-&schema()]
自己实现一个autoreleasepool根据以上原理,我们可以实现一个和系统功能差不多类似的autoreleasepool,源代码在GitHub.
相关知识点苹果对于Tagged Pointer特点的介绍:
Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
在内存读取上有着3倍的效率,创建时比以前快106倍。
为什么要用Tagged Pointer?因为数据类型的长度是和CPU长度有关的,这样就导致了在64位CPU上一些对象占用的内存会翻倍。同时维护程序中的对象需要 分配内存,维护引用计数,管理生命周期,使用对象给程序的运行增加了负担。我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。因为Tagged pointer不是一个真正的对象,如果使用isa指针在编译时会报错。
前言SDWebImage是一个非常好用的图片异步加载的库,在GitHub上目前已经有16984个star。这么优秀的库,我们当然要好好学习一下它的设计和实现,接下来我们就看看SDWebImage的源码是如何实现的。
SDWebImage结构首先我们看看SDWebImage的项目组织结构:
Downloader负责图片的异步下载;
Cache负责图片的缓存;
SDWebImageManager是总的管理类,维护了一个SDWebImageDownloader实例和一个SDImageCache实例,是下载与缓存的桥梁;SDWebImageDecoder负责图片的解压缩;SDWebImagePrefetcher负责图片的预取;
UIImageView+WebCache和其他的扩展负责给用户提供接口;
其中,最重要的就是SDWebImageDownloader、SDImageCache、SDWebImageManager三个类,接下来我们就一步步详细分析一下这些类具体如何实现的。
为了便于大家从宏观上有个把握,我这里先给出项目的框架结构:
从这个图我们明显可以看出来,UIImageView+WebCache和UIButton+WebCache负责为用户提供接口;SDWebImageManger负责协调Downloader和Cache,并且为UIKit层提供支持;最底层的两个类为高层抽象提供支持;我们将采用至顶向下分析的方法,一层层分析。
接口层的实现UIImageView+WebCache这里我们拿UIImageView+WebCache举例,UIButton+WebCache与此类似。UIImageView+WebCache提供的接口如下:
- (void)setImageWithURL:(NSURL *)
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)
- (void)setImageWithURL:(NSURL *)url completed:(SDWebImageCompletedBlock)completedB
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletedBlock)completedB
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletedBlock)completedB
这些接口都调用了
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
这是一种常用的封装的方法,一个函数提供最复杂的实现,其他函数给这个函数在此基础上通过调用的时候提供默认参数,浅浅的封装一层方便用户使用。因为OC函数不支持默认参数(C++支持),所以需要在提供几个函数包装一下。
这个函数的上半部分实现如下:
[self sd_cancelCurrentImageLoad]
objc_setAssociatedObject(self, &imageURLKey, url,
OBJC_ASSOCIATION_RETAIN_NONATOMIC)
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder
在加载一张图片的时候首先取消当前正在加载图片的操作,然后给当前的UIImageView关联一个对应图片的URL,关联的这个url库里本身没有用到,只是暴露出- (NSURL *)sd_imageURL;接口提供给用户使用;if里判断用户是否使用了SDWebImageDelayPlaceholder,如果没有使用就设置placeholder,默认情况没有设置选项,图片在加载完成之前会显示占位图,如果设置了这个选项,会在图片加载完成之后在显示占位图,关于其它选项的具体含义,可以看叶孤城的解析。dispatch_main_async_safe是SDWebImage封装的一个保证参数block在主线程执行的宏,宏的实现比较简单,这里不在解释。
我们具体分析一下取消操作是如何实现的,[self sd_cancelCurrentImageLoad]调用了父类UIView+EMWebCacheOperation的函数:
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"]
我们可以看到,这里的key是写死的,因为一个UIImageView在任何时候至多只能对应一种类型的下载操作(可以同时下载image和highlightedImage,两者的key是不一样的)。网络下载以及缓存都是比较耗费系统资源的操作,这么做可以尽量避免资源浪费。例如UITableView在滑动到需要复用UITableViewCell时,如果此时滑出屏幕之前的图片还未加载完成,就没必要在下载了。我们继续深入看父类UIView+EMWebCacheOperation的实现,sd_cancelImageLoadOperationWithKey的全部实现如下:
NSMutableDictionary *operationDictionary = [self operationDictionary];
id operations = [operationDictionary objectForKey:key];
if (operations) {
if ([operations isKindOfClass:[NSArray class]]) {
for (id &SDWebImageOperation& operation in operations) {
if (operation) {
[operation cancel];
} else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
[(id&SDWebImageOperation&) operations cancel];
[operationDictionary removeObjectForKey:key];
然后我们接着看sd_setImageWithURL的下半部分的实现,截取部分关键的代码:
__weak __typeof(self)wself = self;
id &SDWebImageOperation& operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
[wself removeActivityIndicator];
if (!wself) return;
dispatch_main_sync_safe(^{
if (!wself) return;
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
completedBlock(image, error, cacheType, url);
else if (image) {
wself.image =
[wself setNeedsLayout];
if ((options & SDWebImageDelayPlaceholder)) {
wself.image =
[wself setNeedsLayout];
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
接口层的实现到这里就结束了。这里有几个小问题:
iOS底层如何实现Category关联属性的?
iOS的Category如何关联weak属性?
调度层的实现 调度层就一个类SDWebImageManager,在头文件中描述如下:
The SDWebImageManager is the class behind the UIImageView+WebCache category and likes. It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache). You can use this class directly to benefit from web image downloading with caching in another context than a UIView.
意思就是SDWebImageManager是位于UIImageView+WebCache之下的用于图片异步下载和缓存的类,你也可以直接使用SDWebImageManager的函数直接下载图片。
SDWebImageManager是一个用dispatch_once实现的单例(关于dispatch_once底层如何实现的?也是一个有趣的话题,我们以后在讨论它),维护了一个SDImageCache实例和SDWebImageDownloader实例。提供的函数如下:
//初始化SDWebImageManager单例,在init方法中已经初始化了cache单例和downloader单例。
- (instancetype)initWithCache:(SDImageCache *)cache downloader:(SDWebImageDownloader *)
- (id )downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedB
- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)
- (void)cancelA
- (BOOL)isR
- (BOOL)cachedImageExistsForURL:(NSURL *)
- (BOOL)diskImageExistsForURL:(NSURL *)
- (void)cachedImageExistsForURL:(NSURL *)url
completion:(SDWebImageCheckCacheCompletionBlock)completionB
- (void)diskImageExistsForURL:(NSURL *)url
completion:(SDWebImageCheckCacheCompletionBlock)completionB
- (NSString *)cacheKeyForURL:(NSURL *)
定义了SDWebImageManagerDelegate协议:
* Controls which image should be downloaded when the image is not found in the cache.
* 控制在cache中没有找到image时 是否应该去下载。默认是YES。
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
* Allows to transform the image immediately after it has been downloaded and just before to cache it on disk and memory.
* NOTE: This method is called from a global queue in order to not to block the main thread.
* 在下载之后,缓存之前转换图片。在全局队列中操作,不阻塞主线程
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;
我们主要看下
- (id )downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock
这个函数返回id &SDWebImageOperation&类型保存当前UIImageView的下载操作;
首先检查URL是否合法:
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
if (![url isKindOfClass:NSURL.class]) {
这两个判断都是检查用户传递参数类型是否正确;
BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
dispatch_main_sync_safe(^{
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
若URL对应的image之前下载失败过并且用户没有设置错误重试,直接调用completedBlock;
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
把当前下载操作加入到runningOperations中,因为runningOperations是多线程共享的,所以要加synchronized控制并发;synchronized底层是如何实现的呢?我们在以后的文章里去探究。
SDWebImageManager首先去检查cache(memeory && disk)中是否缓存过要下载的图片,调用imageCache的
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock
函数里首先判断当前操作是否被取消,如果取消从runningOperations中移除,并直接返回:
(operation.isCancelled) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
接下来需要讨论二种情况情况:1.(缓存未找到图片)或(缓存找到了图片但是用户设置了刷新缓存) 2. (用户没有实现shouldDownloadImageForURL协议)或(用户实现了shouldDownloadImageForURL协议并且返回值是YES)
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]))
这两种情况下需要下载图片,上面这一坨就是判断上面的二种情况。
除了上面二种情况还有不需要下载图片的二种情况:1.缓存找到图片 2.缓存未找到图片并且用户禁止下载:
else if (image) {
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakO
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakO
if (strongOperation && !weakOperation.isCancelled) {
completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
如果需要下载图片,就调用
- (id )downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock
下载图片,如果下载失败。直接调用completedBlock返回错误,并根据错误类型将URL添加到failedURLs里。
else if (error) {
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
若图片下载成功,将url从failURLs里删除:
((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
这个地方需要解释下,因为[self.failedURLs addObject:url]是只在下载失败时添加的,而下载成功和下载失败是互斥的,也就是说,下载成功时failedURLs数组里就不应该有这个url,为什么要这么写呢,这是为了解决竞态条件下的问题,若两个线程下载同一个url的图片,若第一个线程下载失败,第二个下载成功。如果不从failedURLs移除这个url的话,以后下载此url的图片都会失败。
然后处理下载完成的图片:
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
if (options & SDWebImageRefreshCached && image && !downloadedImage) {
else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
[self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
下载完成之后,从队列里移除:
(finished) {
@synchronized (self.runningOperations) {
(strongOperation) {
[self.runningOperations removeObject:strongOperation];
最后说下返回的SDWebImageCombinedOperation类型,这个类型包含NSOperation *cacheOperation的一个子类型,其中cacheOperation中又存在id &SDWebImageOperation&的下载图片的subOperation。在cancel的时候也应该把这两个操作都cancle。
- (void)cancel {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
if (self.cancelBlock) {
self.cancelBlock();
this is a temporary fix to #809.
_cancelBlock = nil;
所以CombinedOperation在cancel的时候会先cancel掉自己的cacheOperation,在调用自己的cancelBlock。
operation.cancelBlock = ^{
[subOperation cancel]
__strong __typeof(weakOperation) strongOperation = weakOperation
if (strongOperation) {
[self.runningOperations removeObject:strongOperation];
在自己的cancelBlock把下载操作subOperation取消掉。由此可见封装的CombinedOperation包含了下载和缓存的操作,使代码变得更简洁。
下载和缓存层下载层SDWebImageDownloaderSDWebImageDownloader提供的方法有以下几个:
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)
- (NSString *)valueForHTTPHeaderField:(NSString *)
- (void)setOperationClass:(Class)operationC
- (id )downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedB
- (void)setSuspended:(BOOL)
- (void)cancelAllD
我们重点研究下载方法:
- (id )downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedB
这几个参数的含义代码注释里都有,这里就不再一一解释。
__block SDWebImageDownloaderOperation *
__weak __typeof(self)wself = self;
[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
operation = [[wself.operationClass alloc] initWithRequest:request
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize)
上面是这个方法的框架,可以看出主要调用了addProgressCallback方法,方法的内部创建了下载的operation。我们首先看看addProgressCallback方法的实现:
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
if (!self.URLCallbacks[url]) {
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;
if (first) {
createCallback();
这里的并发控制的地方为什么要使用dispatch_barrier_sync,是因为dispatch_barrier_sync这个函数可以设置一个同步执行block,它会等到在这个block加入队列之前的block执行完毕后,才开始执行。在它之后加入队列的block,则等到这个block执行完毕后才开始执行。。这里的dispatch_barrier_sync实际上相当于一个写锁,任何写操作(删除,添加),都要在之前的写操作完成后执行,而之后的写操作,也要在之前的的写操作完成之后执行。
URLCallbacks是一个以url作为key的字典,字典的value是一个数组,数组的元素是一个字典。可以看出URLCallbacks、callbacksForURL、callbacks之间的关系如下图:
如果URLCallbacks以url对应的value是空,说明是第一次请求这个url,需要调用createCallback创建下载任务,即调用:
- (id)initWithRequest:(NSURLRequest *)request
inSession:(NSURLSession *)session
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock
cancelled:(SDWebImageNoParamsBlock)cancelBlock
设置默认超时时间15s,然后使用此request创建一个SDWebImageDownloaderOperation,方便把这个下载任务添加到下载队列里:
- (id)initWithRequest:(NSURLRequest *)request
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock
cancelled:(SDWebImageNoParamsBlock)cancelBlock
重点看下这个方法的三个block,第一个progress,取出存储在URLCallbacks中的progressBlock并调用:
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
for (NSDictionary *callbacks in callbacksForURL) {
dispatch_async(dispatch_get_main_queue(), ^{
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
同理,completed也是从URLCallbacks中的completedBlock并调用:
dispatch_barrier_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
if (finished) {
[sself.URLCallbacks removeObjectForKey:url];
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
不同图片的下载任务会异步完成,所以要等待其他图片下载完成,并执行完completedBlock中对URLCallbacks的操作,才能继续之后的操作。
cancelled:^{
SDWebImageDownloader *sself = wself
if (!sself) return
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url]
取消时用的dispatch_barrier_async,dispatch_barrier_async表示的是先等之前的执行完成,然后把该barrier放入queue中,而不等待barrier中代码执行结束,而dispat_barrier_sync表示需要等待barrier中代码执行结束。
然后设置operation
operation.shouldDecompressImages = wself.shouldDecompressI
if (wself.urlCredential) {
operation.credential = wself.urlC
} else if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityH
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityL
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation =
最后将这个下载的操作加入到下载队列里。下载方式有两种,FIFO和LIFO。最后根据下载的方式调增任务的依赖。
具体的下载操作SDWebImageDownloaderOperation上面的SDWebImageDownloader主要操作就是创建SDWebImageDownloaderOperation添加到下载队列里,可知具体的操作是在SDWebImageDownloaderOperation,我们研究下具体是如何下载的。首先这个类是继承于NSOperation,并重写了start方法。我们首先看看它的start方法的实现:
检测下载状态
if (self.isCancelled) {
self.finished = YES;
[self reset];
如果是iOS4.0以上的版本,需要设置后台执行的操作:
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself =
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
这里注册的block会在APP进入后台时执行,block里调用cancel对应的endBackgroundTask,这个block的主要目的是持有self不要被系统销毁,只要self不被系统销毁,当前下载操作就可以继续执行。self被销毁后,我们看看cancel操作做了什么:
- (void)cancel {
@synchronized (self) {
if (self.thread) {
[self performSelector:@selector(cancelInternalAndStop) onThread:self.thread withObject:nil waitUntilDone:NO];
[self cancelInternal];
- (void)cancelInternalAndStop {
if (self.isFinished) return;
[self cancelInternal];
CFRunLoopStop(CFRunLoopGetCurrent());
- (void)cancelInternal {
if (self.isFinished) return;
[super cancel];
if (self.cancelBlock) self.cancelBlock();
if (self.connection) {
[self.connection cancel];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
if (self.isExecuting) self.executing = NO;
if (!self.isFinished) self.finished = YES;
[self reset];
底下有三个和cancel相关的操作,如果当前线程存在,在当前线程调用cancelInternalAndStop,否者调用cancelInternal;这两者的区别是cancelInternalAndStop中多了一句CFRunLoopStop(CFRunLoopGetCurrent()),在中止当前线程的时候,也要关闭对应的RunLoop。cancelInternal做了几件事:
调用自定义的cancelBlock
调用NSURLConnection的cancel取消self.connection
注册的后台操作是在进入到后台如果NSOperation未执行完成才执行的,假如在进入到后台之前NSOperation已经完成了呢?
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
如果下载任务完成,中止后台操作,将backgroundTaskId置为UIBackgroundTaskInvalid。
注册后台操作的准备工作完成之后,就该开始下载了:
self.executing = YES;
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
self.thread = [NSThread currentThread];
初始化相关的数据,创建NSURLConnection,如果创建失败就调用completedBlock,成功就启动下载,关键代码如下:
if (self.progressBlock) {
self.progressBlock(0, NSURLResponseUnknownLength);
CFRunLoopRun();
if (!self.isFinished) {
[self.connection cancel];
[self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
使用NSURLConnection调用了[self.connection start]后,NSURLConnection的delegate就会不停收到事件回调。当这个connection完成或者终止,才会跳出CFRunLoopRun()(可以理解为CFRunLoopRun阻塞了当前线程)。当跳出Runloop后,就要判断NSURLConnection是不是正常完成任务了。如果没有,也就是说self.isFinished == NO。那么就取消该connection,并且调用
(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)
返回错误信息。然后就是NSURLConnection几个delegate过程的处理:
if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] & 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) {
NSInteger expected = response.expectedContentLength & 0 ? (NSInteger)response.expectedContentLength : 0;
self.expectedSize =
if (self.progressBlock) {
self.progressBlock(0, expected);
self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
self.response =
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
NSUInteger code = [((NSHTTPURLResponse *)response) statusCode];
if (code == 304) {
[self cancelInternal];
[self.connection cancel];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES);
CFRunLoopStop(CFRunLoopGetCurrent());
[self done];
然后在下载过程中接收数据,以下是部分代码:
if (partialImageRef) {
UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
UIImage *scaledImage = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:scaledImage];
image = scaledI
CGImageRelease(partialImageRef);
dispatch_main_sync_safe(^{
if (self.completedBlock) {
self.completedBlock(image, nil, nil, NO);
因为通过 imageNamed 创建 UIImage 时,系统实际上只是在 Bundle 内查找到文件名,然后把这个文件名放到 UIImage 里返回,并没有进行实际的文件读取和解码。当 UIImage 第一次显示到屏幕上时,其内部的解码方法才会被调用,同时解码结果会保存到一个全局缓存去。在图片解码后,App 第一次退到后台和收到内存警告时,该图片的缓存才会被清空,其他情况下缓存会一直存在。具体的说就是一个UIImage加载了jpeg或者png,当UIImageView将要显示这个UIImage的时候会先把png和jpeg解码成未压缩格式,所以SDWebImage有一个decodeImage方法,就是把这一步放在了异步线程做,防止tableViewCell中的imageView加载图片的时候在主线程解码图片,导致滑动卡顿。这样效率很低,但是只有瞬时的内存需求。为了提高效率通过SDWebImageDecoder将包装在Data下的资源解压,然后画在另外一张图片上,这样这张新图片就不再需要重复解压了,这种做法是典型的空间换时间的做法,如下从硬盘中去图片时,分别对图片进行了缩放和解压缩操作。
缓存层SDImageCache我们看下SDWebImage的缓存SDImageCache,SDImageCache主要包含两部分,内存缓存memCache和磁盘缓存fileManager,磁盘缓存的写操作是异步的。
SDImageCache maintains a memory cache and an optional disk cache. Disk cache write operations are performed asynchronous so it doesn’t add unnecessary latency to the UI.
内存缓存是使用NSCache实现的,NSCache使用上类似字典,可以用key-Value的方式存取数据。但是NSCache底层实现和NSDictionary不同(NSCache是线程安全的)。NSCache的具体介绍可以看。
先看看SDImageCache的属性:
@property (assign, nonatomic) BOOL shouldDecompressI
@property (assign, nonatomic) BOOL shouldDisableiC
@property (assign, nonatomic) BOOL shouldCacheImagesInM
@property (assign, nonatomic) NSUInteger maxMemoryC
@property (assign, nonatomic) NSUInteger maxMemoryCountL
@property (assign, nonatomic) NSInteger maxCacheA
@property (assign, nonatomic) NSUInteger maxCacheS
前面SDWebImageManager在下载图片成功时,会调用
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk]
缓存到内存和磁盘,我们重点看下这个函数的实现:
if (!image || !key) {
if (self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
if (toDisk) {
dispatch_async(self.ioQueue, ^{
NSData *data = imageD
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasA
if ([imageData length] &= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
if (imageIsPng) {
data = UIImagePNGRepresentation(image);
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
if (data) {
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
NSString *cachePathForKey = [self defaultCachePathForKey:key];
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
[_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];
if (self.shouldDisableiCloud) {
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
同样的。SDWebImageManager在下载之前会检查缓存是否有此图片:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock
关键代码如下:
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
@autoreleasepool {
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
最后还有一个Cache清除的方式,对于memory cache来说是完全清空的,对于disk cache,根据设置参数的不同,有两种清除方式:
文件的缓存有效期:默认是一周。如果文件的缓存时间超过这个时间值,则将其移除。
最大缓存空间大小:如果所有缓存文件的总大小超过最大缓存空间,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间。
底层支持(工具类)SDWebImageDecoder和SDWebImageCompatSDWebImageDecoder是用来图片解码的,在上面下载的时候和磁盘读取图片数据时,都调用了解码操作,图片为什么需要解码,可以参考如下解释:
因为通过 imageNamed 创建 UIImage 时,系统实际上只是在 Bundle 内查找到文件名,然后把这个文件名放到 UIImage 里返回,并没有进行实际的文件读取和解码。当 UIImage 第一次显示到屏幕上时,其内部的解码方法才会被调用,同时解码结果会保存到一个全局缓存去。在图片解码后,App 第一次退到后台和收到内存警告时,该图片的缓存才会被清空,其他情况下缓存会一直存在。具体的说就是一个UIImage加载了jpeg或者png,当UIImageView将要显示这个UIImage的时候会先把png和jpeg解码成未压缩格式,所以SDWebImage有一个decodeImage方法,就是把这一步放在了异步线程做,防止tableViewCell中的imageView加载图片的时候在主线程解码图片,导致滑动卡顿。这样效率很低,但是只有瞬时的内存需求。为了提高效率通过SDWebImageDecoder将包装在Data下的资源解压,然后画在另外一张图片上,这样这张新图片就不再需要重复解压了,这种做法是典型的空间换时间的做法,如下从硬盘中去图片时,分别对图片进行了缩放和解压缩操作。
SDWebImageCompat是根据屏幕大小设置图片的scale,实现比较简单,这里不再解释。
相关函数#include &sys/epoll.h&
int epoll_create(int size);
int epoll_create1(int flags);
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
int epoll_wait(int epld,struct epoll_event *events,int maxevents,int timeout);
typedef union epoll_data {
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
epoll_data_t
编程模型epoll有两种触发模式,LT(Level-Triggered,水平触发),ET(Edge–Triggered边缘触发)。其中LT编程模型和poll是一样的。
LT电平触发和ET触发模式的区别是ET触发是高电平到低电平切换的时候或者低电平切换高电平才会触发。
EPOLLIN事件:
内核的输入缓冲区 为空
内核的输入缓冲区 不为空
高电平(一直触发EPOLLIN)
EPOLLOUT事件:
内核发送缓冲区不满
高电平(一直触发EPOLLOUT)
内核发送缓冲区满
LT模型例子#include &unistd.h&
#include &sys/types.h&
#include &fcntl.h&
#include &sys/socket.h&
#include &netinet/in.h&
#include &arpa/inet.h&
#include &signal.h&
#include &fcntl.h&
#include &sys/wait.h&
#include &sys/epoll.h&
#include &stdlib.h&
#include &stdio.h&
#include &errno.h&
#include &string.h&
#include &vector&
#include &algorithm&
#include &iostream&
typedef std::vector&struct epoll_event& EventL
#define ERR_EXIT(m) \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(void)
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
int idlefd = open("dev/null",O_RDONLY | O_CLOEXEC);
if ((listenfd = socket(PF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_TCP)) & 0)
ERR_EXIT("socket");
struct sockaddr_
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
int on = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) & 0)
ERR_EXIT("socksockopt");
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) & 0)
ERR_EXIT("bind");
if (listen(listenfd, SOMAXCONN) & 0)
ERR_EXIT("listen");
std::vector&int&
epollfd = epoll_create1(EPOLL_CLOEXEC);
struct epoll_
event.data.fd =
event.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event);
EventList events(16);
struct sockaddr_
while(true)
nready = epoll_wait(epollfd, &*events.begin(), static_cast&int&(events.size()),-1);
if (-1 == nready)
if (EINTR == errno)
if (0 == nready)
if ((size_t)nready == events.size())
events.resize(events.size() * 2);
for (int i = 0; i & ++i)
if (events[i].data.fd == listenfd)
peerlen = sizeof(peeraddr);
connfd = ::accept4(listenfd, (struct sockaddr*)&peeraddr,
&peerlen, SOCK_NONBLOCK | SOCK_CLOEXEC);
if (-1 == connfd)
if (EMFILE == errno)
close(idlefd);
idlefd = accept(listenfd, NULL, NULL);
close(idlefd);
idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);
ERR_EXIT("accept4");
std::cout&&"ip="&&inet_ntoa(peeraddr.sin_addr)&&
" port="&&ntohs(peeraddr.sin_port)&&std::
clients.push_back(connfd);
event.data.fd =
event.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);
else if (events[i].events & EPOLLIN)
connfd = events[i].data.
if (connfd & 0)
char buf[1024];
int ret = read(connfd, buf, 1024);
if (ret == -1)
ERR_EXIT("read");
if (ret == 0)
std::cout&&"client close"&&std::
close(connfd);
event = events[i];
epoll_ctl(epollfd, EPOLL_CTL_DEL,connfd, &event);
clients.erase(std::remove(clients.begin(), clients.end(), connfd), clients.end());
std::cout&&
write(connfd, buf, strlen(buf));
LT模式下什么时候关注EPOLLOUT事件呢?如果在得到一个套接字马上关注,就会出现busy loop的状态。所以应该在write的时候关注EPOLLOUT事件,如果数据没有写完,我们就需要把未发送完的数据添加到应用层缓冲区,然后关注这个连接套接字的EPOLLOUT事件,等到EPOLLOUT事件到来,取出应用层缓冲区的数据发送,如果应用层缓冲区数据发送完成,取消关注EPOLLOUT事件。流程如下:
ET模型ET表示边缘触发,是电平从低到高或者从高到低才会触发。我们一开始就可以关注EPOLLIN事件和EPOLLOUT事件,不回出现busy loop。所以当接收缓冲区处于高电平状态时,一定要一次性把数据全部读完。因为如果一次没有读完,接收缓冲区仍然处于高电平状态,下次不会在触发EPOLLIN事件。同理,发送缓冲区的处理类似。处理流程如下:
ET模型如何处理EMFILE事件呢?如果accept返回了了EMFILE事件,那么epoll将一直保持在高电平状态,后面新的客户端连接都没有办法处理,需要更复杂的处理逻辑。
select/poll/epoll对比
Linux I/O复用模型Linux下有三种I/O复用模型,select、poll、epoll,前两种在内核中的处理方式是一致的,第三种效率最高。
poll函数原型12#include &poll.h&int poll(struct pollfd *fds, nfds_t nfds, int timeout);
返回值为发生时间描述符的个数。
pollfd结构体定义:
12345struct pollfd { int
short };
poll使用的基本流程
例子123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125#include &unistd.h&#include &sys/types.h&#include &fcntl.h&#include &sys/socket.h&#include &netinet/in.h&#include &arpa/inet.h&#include &signal.h&#include &sys/wait.h&#include &poll.h&#include &stdlib.h&#include &stdio.h&#include &errno.h&#include &string.h&#include &iostream&#include &vector&#define ERR_EXIT(m) \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
typedef std::vector&struct pollfd& PollFdL int main(int argc, char *argv[]){
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN); int
listenfd = socket(PF_INET,SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC,IPPROTO_TCP); if (listenfd & 0)
ERR_EXIT("socket"); struct sockaddr_ memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
int on = 1; if (setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR, &on, sizeof(on)) & 0)
ERR_EXIT("setsockopt"); if (bind(listenfd,(struct sockaddr*)&servaddr, sizeof(servaddr)) & 0)
ERR_EXIT("bind"); if (listen(listenfd,SOMAXCONN) & 0)
ERR_EXIT("listen");
struct pfd.fd = pfd.events = POLLIN;
PollFdL pollfds.push_back(pfd);
struct sockaddr_ socklen_t int
while(1) {
nready = poll(&*pollfds.begin(),pollfds.size(),-1);
if (nready == -1)
if (errno == EINTR)
ERR_EXIT("poll");
if (pollfds[0].revents & POLLIN)
peerlen = sizeof(peeraddr);
connfd = ::accept4(listenfd, (struct sockaddr*)&peeraddr,
&peerlen, SOCK_NONBLOCK | SOCK_CLOEXEC);
if (connfd == -1)
ERR_EXIT("accept4");
pfd.events = POLLIN;
pfd.revents = 0;
pollfds.push_back(pfd);
std::cout&&"ip="&&inet_ntoa(peeraddr.sin_addr)&&
"port="&&ntohs(peeraddr.sin_port)&&std::
if (nready == 0)
std::cout&&nready&&std::
for (PollFdList::iterator it = pollfds.begin()+1; it != pollfds.end() && nready & 0;++it)
if (it-&revents && POLLIN)
connfd = it-&
char buf[1024] = {0};
int ret = read(connfd, buf, 102);
if (ret == -1)
ERR_EXIT("read");
if (ret == 0)
std::cout&&"client close"&&std::
= pollfds.erase(it);
close(connfd);
std::cout&&
write(connfd, buf, strlen(buf));
} } return 0;}
问题与改进如果服务器主动断开连接(先于client调用close),服务端就会进入time_wait状态。协议设计上,应该让客户端主动断开连接,这样就把TIME_WAIT状态分散到大量的客户端。但是如果一些恶意客户端一直不断开连接,这样就会占用服务器端的连接资源,所以服务端需要机制踢掉不活跃的连接。
完善read和write上面的代码在实际使用的时候有几个问题。一、read操作可能一次不能把对应fd所对应的缓冲区的数据读完,这时fd仍然是活跃的。二、假设应答的数据量比较大,一次write操作不一定能把所有业务数据写到内核缓冲区中。解决办法就是对于read和write操作都建立对应的缓冲区,等数据完全发送/接收完成之后在进行业务操作。
对于write操作来说,需要在数据未完全发送时关注connfd的POLLOUT事件(如果一开始就关注connfd的POLLOUT事件,connnfd会不停的产生事件,造成忙等待)。等应用层缓存区中的数据完全发送完成之后,取消关注connfd的POLLOUT事件。
accept返回EMFILE的处理EMFILE是打开的文件描述符的个数超出了上限。解决办法有以下几种:
调高进程文件描述符个数(不能解决根本问题,不推荐)
死等,直到有可用的文件描述符(不推荐)
关闭监听套接字(不现实,什么时候重新打开呢?)
如果是epoll模型,可以改用edge trigger。问题是如果漏掉一次accept,程序再也不会收到新连接。
准备一个空闲的文件描述符。遇到这种情况,先关闭这个空闲文件,获得一个可用的描述符,再accept拿到socket连接的文件描述符;随后立即调用close,这样就优雅的断开了与客户端的链接;最后重新打开空闲文件,以备再次出现这种情况使用。这是比较常用的处理方法。
最后一种方式处理的代码如下:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150#include &unistd.h&#include &sys/types.h&#include &fcntl.h&#include &sys/socket.h&#include &netinet/in.h&#include &arpa/inet.h&#include &signal.h&#include &sys/wait.h&#include &poll.h&#include &stdlib.h&#include &stdio.h&#include &errno.h&#include &string.h&#include &vector&#include &iostream&#define ERR_EXIT(m) \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)typedef std::vector&struct pollfd& PollFdLint main(void){ signal(SIGPIPE, SIG_IGN); signal(SIGCHLD, SIG_IGN); int idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC); int
if ((listenfd = socket(PF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_TCP)) & 0)
ERR_EXIT("socket"); struct sockaddr_ memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); int on = 1; if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) & 0)
ERR_EXIT("setsockopt"); if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) & 0)
ERR_EXIT("bind"); if (listen(listenfd, SOMAXCONN) & 0)
ERR_EXIT("listen"); struct pfd.fd = pfd.events = POLLIN; PollFdL pollfds.push_back(pfd); int struct sockaddr_ socklen_t int while (1) {
nready = poll(&*pollfds.begin(), pollfds.size(), -1);
if (nready == -1)
if (errno == EINTR)
ERR_EXIT("poll");
if (nready == 0)
if (pollfds[0].revents & POLLIN)
peerlen = sizeof(peeraddr);
connfd = accept4(listenfd, (struct sockaddr*)&peeraddr,
&peerlen, SOCK_NONBLOCK | SOCK_CLOEXEC);
ERR_EXIT("accept4");*/
if (connfd == -1)
if (errno == EMFILE)
close(idlefd);
idlefd = accept(listenfd, NULL, NULL);
close(idlefd);
idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);
ERR_EXIT("accept4");
pfd.events = POLLIN;
pfd.revents = 0;
pollfds.push_back(pfd);
std::cout&&"ip="&&inet_ntoa(peeraddr.sin_addr)&&
" port="&&ntohs(peeraddr.sin_port)&&std::
if (nready == 0)
for (PollFdList::iterator it=pollfds.begin()+1;
it != pollfds.end() && nready &0; ++it)
if (it-&revents & POLLIN)
connfd = it-&
char buf[1024] = {0};
int ret = read(connfd, buf, 1024);
if (ret == -1)
ERR_EXIT("read");
if (ret == 0)
std::cout&&"client close"&&std::
it = pollfds.erase(it);
close(connfd);
std::cout&&
write(connfd, buf, strlen(buf));
} }
return 0;}
前言上一篇大并发服务器架构大致讲了下大并发服务器架构演变,这一章以大型网站为例,介绍下大并发服务器架构演变的过程。
Web Server和Database Server分离网站刚开始建站时,访问量比较小,可以将web服务器和数据库服务器部署在同一台服务器上。此时Web服务器和数据库服务器互相影响。任何一个出现性能瓶颈,都会影响网站的访问速度。不符合大并发服务器的高性能的要求。任何一个瘫痪,都会导致整个网站瘫痪,不符合大并发服务器的高可用要求。我们可以将Web Server和Database Server分离:
对Web资源的请求从请求方式上来说可以分为对静态资源和对动态资源的请求:
静态请求:html,js,css,image等。这些请求由HTTP服务器(apache,nginx)处理
动态请求:jsp,PHP,asp.Net等。这些请求由应用服务器(JBoss,Tomcat)处理。对静态资源的访问不需要处理,而对动态资源的请求需要业务逻辑处理后返回,所以两者的访问速度是不一样的。为了提高性能,可以将Web服务器进行动静资源分离:
浏览器缓存浏览器缓存是将访问的文件保存在客户端,在同一个会话过程中会检查缓存的副本是否足够新,在后退网页时,访问过的资源可以从浏览器缓存中拿出使用。减少服务器处理请求的数量,提高访问速度。浏览器缓存状态是由header参数控制的,header的参数有四种:
Cache-Control 取值如下:
max-age(单位为s)指定设置缓存最大的有效时间,定义的是时间长短。
s-maxage(单位为s)同max-age,只用于共享缓存(比如CDN缓存)
public 指定响应会被缓存,并且在多用户间共享。
private 响应只作为私有的缓存
no-cache 指定不缓存响应,表明资源不进行缓存.但是设置了no-cache之后并不代表浏览器不缓存,而是在缓存前要向服务器确认资源是否被更改。
no-store 绝对禁止缓存
must-revalidate指定如果页面是过期的,则去服务器进行获取。
Expires 缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。
Last-modified 服务器端文件的最后修改时间,需要和cache-control共同使用,是检查服务器端资源是否更新的一种方式。
ETag 根据实体内容生成一段hash字符串,标识资源的状态,由服务端产生。
前端页面缓存(Front Page Cache,squid)与浏览器缓存将页面缓存在客户端不同,squid是通过代理将页面缓存在单独的服务器上。它是这样实现其功能的,当客户端请求一个页面时,首先请求Squid服务器,Squid随之连接到远程服务器并向这个页面发出请求,然后Squid把页面返回到客户端机器同时本地缓存一份。当下一次某个客户端需要同样的数据的时候,Squid就从缓存中直接取出发送给客户端。
前面提到的缓存技术,都是缓存整个页面的。但是有时候会存在某些页面,这些页面中的一部分是可静态的,一部分是动态的,这时候就需要用到动态内容缓存技术。动态内容缓存技术大致有三种,CSI,SSI,ESI。这里讲下ESI,其中Squid就有支持ESI的模块。
ESI是一种简单的标识语言,开发人员可以使用它标志内容片断以便通过相应的Cache服务器来加速缓存。同时ESI还定义了一 套内容效验标准,可以实现原服务器对Cache服务器中缓存内容的管理,提高了网站对内容的控制能力。CDN网络也可以利用在分布全国各地的节点中安装支 持ESI的Cache服务器来提供对网站动态内容提供CDN服务。
举个例子,比如网站首页大部分内容都是静态的,但是会在登录之后显示某某,欢迎你的字样的动态内容。如果因为这一点动态内容不缓存页面就太可惜了。用ESI可以做如下处理:
123456&esi:include src="/welcome" /&&ul&#foreach($user in $users)&li&$user.name&/li&#end&/ul&
缓存服务器会将整个页面cache,然后它发现这儿有个ESI标签,它会根据src指定的地址去源服务器请求内容,然后将其合并到缓存的页面中,然后将完整的内容发送到客户端。整个过程类似下图所示:
本地数据缓存缓存数据库的查询结果到数据库服务器。
Web Server集群前端负载均衡
DNS负载均衡在DNS服务器中,可以为多个不同的地址配置同一个名字,对于不同的客户机访问同一个名字,得到不同的地址。
反向代理使用代理服务器将请求发给内部服务器,让代理服务器将请求均匀转发给多台内部web服务器之一,从而达到负载均衡的目的。标准代理方式是客户使用代理访问多个外部Web服务器,而这种代理方式是多个客户使用它访问内部Web服务器,因此也被称为反向代理模式。
基于NAT的负载均衡技术简单的来说,NAT负载均衡是将外网IP地址转换为内部IP地址,将外网大量的访问合理的分配到内部的多台服务器上,从而降低服务器访问压力。
LVSLinux虚拟服务器负载均衡。
F5硬件负载均衡
应用服务器负载均衡增加任务服务器,由任务服务器调度业务服务器实现负载均衡。上一篇已经讲过,这里不在详细讲解。
数据库负载均衡通过构建数据库服务器集群实在数据库服务器负载均衡。
CDN、分布式缓存、分库分表CDN在各个运营商不同地区增加内容分发网络,这样不同地区、不同运营商接入都能提升访问数据。
分布式缓存增加分布式缓存替代本地缓存,让所有的机器共享缓存。目前流行的分布式缓存方案有memcached、membase、redis。基本上当前的NoSql方案都可以用作分布式缓存方案。分布式缓存结构如下:
数据库分库分表当数据库数据量非常大的时候,为了将数据库查询均衡到不同的服务器上,需要对数据库进行分区,数据库分区分为以下两种方式:
垂直分区当服务器并发量比较大的时候,数据库访问就会容易出现锁竞争,而锁竞争是数据库访问性能的杀手, 所以,我们可以将不同业务数据分离到不同的数据库。如图:
水平分区数据库中有部分表可能数据量非常大,可以将数据库大表中的数据分摊到不同的数据库中对应的表。不同数据库中对应表的数据总和就是这张表对应的所有数据。数据分摊的方式有不同的实现,一般都使用hash算法。如图:
数据库分库分表之后数据库物理结构发生了改变,不能在像之前直接访问,需要增加DAL数据访问层。
多数据中心+分布式存储与计算增加多数据中心和分布式存储与计算后的结构图
分布式存储大型网站当中有一些数据对数据的一致性要求不高,这些数据没有必要保持在关系型数据库中。关系型数据库对关系的一致性要求比较高,数据库的事物,大表的join都是数据库性能的杀手。我们可以把这些数据分离出来,把这些数据放到数据中心里。数据中心一般都使用NoSql数据库,NoSql采用key/value存储数据。大并发的性能比关系型数据库的性能要高。
NoSql的数据一般存在于分布式的文件系统中,为什么要使用自己的文件系统呢?举个淘宝例子,淘宝商品有很多小图片,假如放在操作系统管理的文件系统中,操作系统文件系统每一个块都比较小,这样查找某个图片时需要多次查找块,这个过程需要多次移动磁头,所以整体是比较耗时。假如把每一个块都增大,将相关性比较大的数据存到块里,每个块包含一些存储数据的信息。通过hash算法,就能快速找到文件。目前比较有名的分布式文件系统有:Lustre,HDFS,GFS,TFS,FreeNas等。
分布式计算Map/Reduce假如我们要统计一些文件中每个单词出现的的个数。可能文件非常多,无法全部加载到内容中。我们可以使用多台机器来统计。每台机器统计一部分,把统计的结果暂时保存到key/value数据库中,这个过程称作Map。
Map完成之后需要将相同结果合并,比如a机器出现单词hello3次,b机器出现4次,那么reduce服务器需要把统计结果相加为hello出现7次,合并的过程称作reduce。
Map/Reduce的过程如下:
key-value数据库,作为NoSql的解决方案,基本都支持Map/Reduce算法。
服务器设计目标高性能(High Performance)对于大量的并发请求,服务器能否及时快速的做出响应,这就要求我们编写的程序能够最大发挥出机器的性能,能够在短时间处理尽可能多的的并发请求。
高可用(High Availability)要求服务器能够提供7x24小时不间断的服务,如果某台服务器出现故障,也能自动转移到备用机,而不需要人工干预(faileover机制)。
伸缩性(Scalability)服务器采用良好的架构,分层设计,业务分离,能够进行灵活的部署。比如服务器有A,B两个组件,这两个组件可以部署在同一台服务器上,也可以部署在不同的服务器上,这就要求这两个组件不能使用本地通信机制,比如共享内存。而应该使用TCP这样的跨机器的通信方式。
典型的服务器结构常用的软件体系结构有C/S和B/S两种,C/S是指客户端-服务器模型,B/S是指浏览器-服务器模型,因为浏览器也是客户端程序,所以说本质上也是C/S结构。下图是C/S结构图。
服务器并发的影响因素网络I/O+服务器高性能编程技术对于Linux来说,网络I/O最高效的编程模型是epoll。对于服务器编程来说,耗时操作有以下几个类型:
数据拷贝 比如数据从内核拷贝到应用层,一般通过缓存常用的数据解决。
环境切换 线程切换,对于单核服务器来说,使用多线程不一定能提高性能,采用状态机模型解决最好。
内存分配 增加内存池,减少向操作系统申请内存。
尽量减少锁竞争。
数据库限制
数据库连接数
假如数据库并发连接数是10个,假设应用服务器有1000个连接请求,将会有990个请求失败。
数据库连接时限
假如数据库并发连接数10个,假设数据库1s之内最多处理1000个请求,如果应用服务器有1w个请求,那么处理全部请求需要10s。如果系统规定最大响应时间是5s,那么当前数据库最大并发量是5000。
提高服务器性能降低数据库访问压力
增加DAL队列 + 连接池。为了解决这个问题,可以增加一个DAL队列,大量数据库访问的时候,我们可以让当前不能处理的连接排队。为了提升性能,也可以增加一个数据库连接池,连接池创建的时候申请一些数据库连接放到池里,当下一次需要数据库连接的时候直接从连接池中取出处理。如下图:
尽量将业务逻辑挪到应用服务器,数据库少做业务处理。因为在数据库上计算占用CPU,不如在操作系统上业务处理效率高。
缓存数据。把常用的数据缓存起来,下一次请求时从缓存中取数据。降低对数据库请求的次数。
缓存算法。常用缓存算法有FIFO、LRU、LFU,最近开源的nosql数据库一般都会实现数据内存缓存,例如分布式缓存框架redis、memcached。
缓存的实效性。另外缓存是有时效性的,使用缓存的时候会有同步的问题。一般有两种处理方案。一、缓存失效时,我们需要重新去数据库查询。这种方案实时性比较差。二、更新数据库时,将一些热点数据同步更新至缓存。这种方案实时性比较高,但是实现起来比较麻烦。
数据库负载均衡。大部分情况下,数据库读操作多于写操作。这时候就应该对数据进行数据库读写分离,也就是数据库的负载均衡。目前主流的数据库都具有replication进行负载均衡。(Master-Slaver模式)
分库分表。因为不同类型的数据读写速度是不一样的,把不同数据分离能显著提升数据库性能。分库是指数据库可以按照一定的逻辑把表分散到不同的数据库(垂直分区),比如我们可以把一个数据库分为用户表、业务表、基础表组成的库。另外一种比较常用的方法是水平分区,每个数据库都有对应的表,只是把对应表里的记录分割到不同的数据库,}

我要回帖

更多关于 epoll et lt 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信