移动开发技术

Jason's Blog

SDWebImage源码解读

SDWebImage是最流行的iOS第三方图片加载库,也是github上star数目最多的objective-c第三方库。这篇文章对SDWebImage的源码进行简单的分析,主要是分析代码的执行流程。源码版本是目前最新的稳定版本4.4.4。

源码目录

SDWebImage下分为以下几个目录

  • Downloader: 负责图片的下载,基于NSURLSession实现,主要类是SDWebImageDownloader
  • Cache: 负责图片的缓存,主要是对内存和磁盘进行读写,实现二级缓存功能,主要类是SDImageCache
  • Decoder: 负责图片的解码功能,以支持不同格式的图片,例如SDWebImageGIFCoder是对GIF图片的支持
  • Utils: 封装的工具类,主要类是SDWebImaegeManger,控制整个图片下载和缓存流程
  • Categories: 一些扩展支持,比如如果要对GIF图片进行下载展示,需要引入UIImage+GIF.h(框架已默认引入)
  • WebCache Categories: 对UIView的扩展支持,最常用的是UIImageView+WebCache.h,实现了UIImageView的图片加载和缓存功能
  • FLAnimatedImage: 对FLAnimatedImage进行了扩展,可以对动态图片进行加载和缓存

调用时序图

以官方Demo详情页加载图片为例,加载图片的时序图如下:

  1. 调用sd_internalSetImageWithURL:placeholderImage:options:operationKey:internalSetImageBlock:progress:completed:context:
  2. 调用loadImageWithURL:options:progress:completed:
  3. 调用queryCacheOperationForKey:cacheOptions:done查询缓存
  4. 调用imageFromMemoryCacheForKey查内存, 调用diskImageDataBySearchingAllPathsForKey查磁盘
  5. 调用downloadImageWithURL:options:progress:completed:从网络下载
  6. 调用storeImage:imageData:forKey:toDisk:completion:将结果缓存

源码分析

在官方Demo中,有一个列表页和详情页,我们从更简单的详情页来分析,详情页只有一个FLAnimatedImage控件,功能就是加载了一张图片,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__weak typeof(self) weakSelf = self;
[self.imageView sd_setImageWithURL:self.imageURL
                  placeholderImage:nil
                           options:SDWebImageProgressiveDownload
                          progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL *targetURL) {
                              dispatch_async(dispatch_get_main_queue(), ^{
                                  float progress = 0;
                                  if (expectedSize != 0) {
                                      progress = (float)receivedSize / (float)expectedSize;
                                  }
                                  weakSelf.progressView.hidden = NO;
                                  [weakSelf.progressView setProgress:progress animated:YES];
                              });
                          }
                         completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
                             weakSelf.progressView.hidden = YES;
                             [weakSelf.activityIndicator stopAnimating];
                             weakSelf.activityIndicator.hidden = YES;
                         }];

对外暴露的接口比较简单,传递需要加载图片的url,placeholder图片,加载选项options,加载过程回调progressBlock,完成回调completedBlock就可以了。

接下来到时序图的第(1)步,调用UIView+WebCachesd_internalSetImageWithURL:方法,代码如下:

1
2
3
4
5
6
7
8
9
10
[self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                        operationKey:nil
               internalSetImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
                           ……
                       }
                            progress:progressBlock
                           completed:completedBlock
                             context:@{SDWebImageInternalSetImageGroupKey: group}];

继续跟进这个方法,时序图到第(2)步,调用SDWebImageManagerloadImageWithURL:方法,代码如下:

1
2
3
4
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
  //加载完图片的回调
  ……
}

时序图来到第(3)步,SDWebImageManager会调用SDImageCachequeryCacheOperationForKey来进行缓存查询

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
//SDImageCache.m
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {

    // 读内存缓存操作
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }

    NSOperation *operation = [NSOperation new];
    void(^queryDiskBlock)(void) =  ^{
      //读磁盘缓存
      ……
    };

    if (options & SDImageCacheQueryDiskSync) {
        queryDiskBlock();
    } else {
        dispatch_async(self.ioQueue, queryDiskBlock);
    }

    return operation;
}

可以看到,在上边的代码中,调用imageFromMemoryCacheForKey先从内存里查询是否有图片缓存,调用dispatch_async(self.ioQueue, queryDiskBlock)从磁盘里查询是否有图片缓存。这是时序图中的第(4)步。如果命中了缓存,则直接回调完成block,不再走下边的流程了。如果没有命中缓存,那么继续下边。

流程来到时序图中的第(5)步,查询完缓存返回后,SDWebImageManager会调用SDWebImageDownloaderdownloadImageWithURL:方法从网络下载图片

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
//SDWebImageDownloader.m
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {

    LOCK(self.operationsLock);
    NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
    // There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`.
    if (!operation || operation.isFinished || operation.isCancelled) {
        operation = [self createDownloaderOperationWithUrl:url options:options];
        __weak typeof(self) wself = self;
        operation.completionBlock = ^{
            __strong typeof(wself) sself = wself;
            if (!sself) {
                return;
            }
            LOCK(sself.operationsLock);
            [sself.URLOperations removeObjectForKey:url];
            UNLOCK(sself.operationsLock);
        };
        [self.URLOperations setObject:operation forKey:url];
        // Add operation to operation queue only after all configuration done according to Apple's doc.
        // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
        //执行下载操作
        [self.downloadQueue addOperation:operation];
    }
    UNLOCK(self.operationsLock);

    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];

    SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
    token.downloadOperation = operation;
    token.url = url;
    token.downloadOperationCancelToken = downloadOperationCancelToken;

    return token;
}

从网络下载图片的代码如上,第8行生成NSOperation,第26行加入叫做downloadQueueNSOperationQueue执行。

下载完成后,时序图流程来到第(6)步,SDWebImageManager会调用SDImageCachestoreImage:方法将结果进行缓存,以便下次使用,然后再进行成功的回调,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

if (transformedImage && finished) {
    BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
    NSData *cacheData;
    // pass nil if the image was transformed, so we can recalculate the data from the image
    if (self.cacheSerializer) {
        cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
    } else {
        cacheData = (imageWasTransformed ? nil : downloadedData);
    }
    [self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
}

[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];

到这里图片加载流程结束。

SDWebImage里还有很多实现细节,比如多线程的控制和加锁,各种控制下载行为的选项,取消正在下载的操作,URL参数的容错,PlaceHolder的设置,加载IndicatorView的显示控制,下载过程Progress的控制等。由于篇幅原因在本文中省略,等以后有时间再细致分析。

Aspects源码解读

Aspects是iOS面向切面编程的第三方库,它可以在不改变原有代码的情况下,在任意函数之前或之后插入代码,也可以替换掉函数原有的代码。它的原理是基于oc语言的runtime,这篇文章对Aspects进行源码解读,并阐述其原理。

调用方式

首先我们下载官方demo,从入口代码开始看:

1
2
3
4
AspectsViewController *aspectsController = [AspectsViewController new];
[aspectsController aspect_hookSelector:@selector(buttonPressed:) withOptions:0 usingBlock:^(id info, id sender) {
    NSLog(@"Button was pressed by: %@", sender);
} error:NULL];

这段代码就是Aspects的调用方式之一,表示在对象aspectsController的buttonPressed函数执行之后,再执行block里的代码,打印一行日志。withOptions的参数写的0,这里是一个枚举值,可以控制block代码怎样执行,具体的定义如下:

1
2
3
4
5
6
7
typedef NS_OPTIONS(NSUInteger, AspectOptions) {
    AspectPositionAfter   = 0,            /// Called after the original implementation (default)
    AspectPositionInstead = 1,            /// Will replace the original implementation.
    AspectPositionBefore  = 2,            /// Called before the original implementation.

    AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution.
};

hook过程

我们从入口函数进入开始跟踪代码,最后发现无论是对实例方法还是类方法进行hook,都会调用aspect_add函数,省略了一些无关代码后如下:

1
2
3
4
5
6
7
8
9
10
11
12
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
    __block AspectIdentifier *identifier = nil;
    AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
    identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
    if (identifier) {
        [aspectContainer addAspect:identifier withOptions:options];

        // Modify the class to allow message interception.
        aspect_prepareClassAndHookSelector(self, selector, error);
    }
    return identifier;
}

这段代码做了两件事情。

首先生成AspectIdentifier,然后将AspectIdentifier加入到AspectsContainer中。AspectIdentifier的定义如下,它描述了一个Ascpect切片代码的信息。

1
2
3
4
5
6
7
@interface AspectIdentifier : NSObject
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) id block;
@property (nonatomic, strong) NSMethodSignature *blockSignature;
@property (nonatomic, weak) id object;
@property (nonatomic, assign) AspectOptions options;
@end

AspectsContainer的定义如下,它负责容纳AspectIdentifier,可以在before,instead,after数组里放入多个AspectIdentifier,从名称可以看出这些AspectIdentifier所执行的时机。AspectsContainer将在后边取出并执行。

1
2
3
4
5
@interface AspectsContainer : NSObject
@property (atomic, copy) NSArray *beforeAspects;
@property (atomic, copy) NSArray *insteadAspects;
@property (atomic, copy) NSArray *afterAspects;
@end

其次调用aspect_prepareClassAndHookSelector函数,这是最关键的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    Class klass = aspect_hookClass(self, error);
    Method targetMethod = class_getInstanceMethod(klass, selector);
    IMP targetMethodIMP = method_getImplementation(targetMethod);
    if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
        // Make a method alias for the existing method implementation, it not already copied.
        const char *typeEncoding = method_getTypeEncoding(targetMethod);
        SEL aliasSelector = aspect_aliasForSelector(selector);
        if (![klass instancesRespondToSelector:aliasSelector]) {
            __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
        }
        // We use forwardInvocation to hook in.
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
    }
}

这个函数分为两部分,第2行aspect_hookClass和后边的部分。我们先来看aspect_hookClass函数,省略后的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static Class aspect_hookClass(NSObject *self, NSError **error) {
  Class statedClass = self.class;
  Class baseClass = object_getClass(self);
  NSString *className = NSStringFromClass(baseClass);

    // Default case. Create dynamic subclass.
  const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
  Class subclass = objc_getClass(subclassName);

  if (subclass == nil) {
      subclass = objc_allocateClassPair(baseClass, subclassName, 0);
      aspect_swizzleForwardInvocation(subclass);
      aspect_hookedGetClass(subclass, statedClass);
      aspect_hookedGetClass(object_getClass(subclass), statedClass);
      objc_registerClassPair(subclass);
  }

  object_setClass(self, subclass);
  return subclass;
}

第11行代码通过运行时的函数objc_allocateClassPair定义了一个新的子类。如果是demo执行到这里的话,生成的子类叫AspectsViewController_Aspects。第12行,将子类的forwardInvocation替换为了自定义的实现函数__ASPECTS_ARE_BEING_CALLED__。第18行,将AspectsViewController实例的isa指针指向了子类AspectsViewController_Aspects。

接着,我们继续看aspect_prepareClassAndHookSelector函数的后半部分。第10行在AspectsViewController_Aspects类添加了一个方法aliasSelector,demo中就是aspect_buttonPressed,它的实现指向了原来AspectsViewController类的buttonPressed的实现。第13行,将AspectsViewController_Aspects类的buttonPressed实现指向了_objc_msgForward,这样调用就会启动oc的消息转发机制。

到这里,Aspects的hook流程就执行完了,我们用下边这个图来描述下当前类和方法实现之间的关系。

Aspects的实现为什么要生成一个原有类的子类,个人理解是为了对原有类产生的影响尽可能小。

hook后的执行流程

hook完成后,我们来看下hook后代码的执行流程。

这一段很重要!!!往AspectsViewController实例发送buttonPressed消息的时候,首先应该去查找实例所对应的类的方法列表,由于AspectsViewController的isa指向了AspectsViewController_Aspects类,就会去AspectsViewController_Aspects类中查找,结果是查找不到buttonPressed实现,然后会去查找父类AspectsViewController的方法列表,这时候查找到了buttonPressed的实现,但是实现是指向了_msg_forward,这样就进入了消息转发流程。按照消息转发流程,系统会调用AspectsViewController_Aspects类的forwardInvocation方法,forwardInvocation方法被我们替换成了自定义实现__ASPECTS_ARE_BEING_CALLED__,最终就进入了这个方法。

__ASPECTS_ARE_BEING_CALLED__的省略后的代码如下:

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
// This is the swizzled forwardInvocation: method.
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
    SEL originalSelector = invocation.selector;
    SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
    invocation.selector = aliasSelector;
    AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
    AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
    AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];
    NSArray *aspectsToRemove = nil;

    // Before hooks.
    aspect_invoke(classContainer.beforeAspects, info);
    aspect_invoke(objectContainer.beforeAspects, info);

    // Instead hooks.
    BOOL respondsToAlias = YES;
    if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
        aspect_invoke(classContainer.insteadAspects, info);
        aspect_invoke(objectContainer.insteadAspects, info);
    }else {
        Class klass = object_getClass(invocation.target);
        do {
            if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
                [invocation invoke];
                break;
            }
        }while (!respondsToAlias && (klass = class_getSuperclass(klass)));
    }

    // After hooks.
    aspect_invoke(classContainer.afterAspects, info);
    aspect_invoke(objectContainer.afterAspects, info);
}

第7行,对于hook的实例方法,先拿到之前设置的切片代码信息,存储在classContainer里。第24行,通过invocation调用AspectsViewController_Aspects的aspect_buttonPressed方法,由于这个方法已经指向了原来的实现buttonPressed,所以就调用了原始的代码。在这之后,如果Container里有afterAspects,就调用切片的block。beforeAspects同理。

到此为止,就实现了在原来的实例方法执行后,再执行hook插入的block代码。

总结

oc语言的runtime是黑魔法,运用起来可以做很多强大的功能。总的来讲,Aspects利用了method swizzling和消息转发机制forwordInvocation,实现了对函数进行切面hook。

对JSPatch原理的理解

JSPatch利用OC语言的动态特性,让OC语言根据传入的JS代码,进行动态行为修改,以达到热更新的目的。

项目中根据JSPatch的原理,自己实现了一套简单的热更新方案。以替换方法实现为例,热更新运行的步骤如下:

1.补丁下发阶段

应用启动的时候,会加载下发的补丁js文件,以下这一段热更新代码会被执行。

1
2
3
4
replaceMethod("IGTabBarController", "onNaviBarTaskBoxClick:", false, function (invocation) {
    log("origin method");
    callOriginMethod(invocation, "origin_onNaviBarTaskBoxClick:");
});

OC中的JSContext在初始化的时候加载过replaceMethod函数,所以会调用到OC代码

1
ocReplaceMethod:(NSString *)className selectorName:(NSString *)selectorName isClass:(BOOL)isClass func:(JSValue *)func

这个函数里,做了一个重要的逻辑(这里参考了JSPatch的实现方式),将IGTabBarController的实例方法onNaviBarTaskBoxClick:指向了forwardInvocation:,然后自定义实现PMDForwardInvocation替换forwardInvocation:的行为

2.用户调用阶段

用户操作点击后,IGTabBarController的onNaviBarTaskBoxClick:会被执行,从而PMDForwardInvocation被执行,根据OC的函数转发特性,PMDForwardInvocation会拿到所有的函数参数信息invocation。然后调用jsfunc(@[invocation])。这样就将所有原生参数通过invocation对象传回给了js代码。js代码拿到这些参数就可以去实现任何逻辑了,以达到替换原方法的目的。

总结:这里是一个很重要的技巧,如何将需要动态更新的OC方法的参数全部传给js代码,JSPatch是利用了forwardInvocation的特性。

Express学习笔记(二)-路由

这一篇主要介绍路由的用法

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var express = require('express');
var app = express();

// GET method route
app.get('/', function (req, res) {
  res.send('GET request to the homepage');
});

// POST method route
app.post('/', function (req, res) {
  res.send('POST request to the homepage');
});

app.all('/secret', function (req, res, next) {
  console.log('Accessing the secret section ...');
  next(); // pass control to the next handler
});

all表示接受所有的http方法

路由路径

支持正则表达式

1
2
3
app.get('/ab*cd', function(req, res) {
  res.send('ab*cd');
});

此路由路径将匹配 abcd、abxcd、abRABDOMcd、ab123cd 等。

路由处理顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var cb0 = function (req, res, next) {
  console.log('CB0');
  next();
}

var cb1 = function (req, res, next) {
  console.log('CB1');
  next();
}

app.get('/example/d', [cb0, cb1], function (req, res, next) {
  console.log('the response will be sent by the next function ...');
  next();
}, function (req, res) {
  res.send('Hello from D!');
});

可以写数组,也可以写多个回调函数参数,挨着执行,前一个函数必须执行next(),否则http请求会挂起

模块封装

可以封装一个bird.js模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var express = require('express');
var router = express.Router();

// middleware that is specific to this router
router.use(function timeLog(req, res, next) {
  console.log('Time: ', Date.now());
  next();
});
// define the home page route
router.get('/', function(req, res) {
  res.send('Birds home page');
});
// define the about route
router.get('/about', function(req, res) {
  res.send('About birds');
});

module.exports = router;

使用模块后,url从外部使用模块开始,接着模块定义的路径,就可以访问

1
2
3
var birds = require('./birds');
...
app.use('/birds', birds);

此时,可以相应/birds/birds/about

Express学习笔记(一)-开始

Express是一个基于nodejs的web开发框架。

Hello World

1.安装

1
2
3
4
$ mkdir myapp
$ cd myapp
$ npm init
$ npm install express --save

2.修改app.js,然后运行

1
$ node app.js

打开浏览器,可以看到运行结果了

脚手架工程

1.安装express-generator

1
$ npm install express-generator -g

2.生成脚手架工程

1
$ express --view=pug myapp

pug是使用的模板引擎

1
2
$ cd myapp
$ npm install

3.运行

1
$ npm start

在浏览器里访问。

路由

1
2
3
app.get('/', function (req, res) {
  res.send('Hello World!');
});

表示根目录相应http get方法

静态文件

1
app.use('/static', express.static(__dirname + '/public'));

public目录下的所有文件可以作为静态资源访问。

TIP项目网络框架梳理

前段时间小伙伴重构了项目的网络层代码,将之前的过程式的代码,面向对象化了,职责分离,更易维护。不过也增加了理解成本,这里记录一下。

整个网络模块的类图如下。PMD开头的类下沉到了基础库,IG开头的类仍然在项目中。

  • IGNetworkManager作为项目中使用网络层的入口类,不多做介绍。
  • PMDNetworking是发起网络请求的类,这里首先要用PMDCallFactory工厂类,生成一个实现PMDCall协议的对象,然后调用makeCallWithRequest方法进行网络请求。
  • PMDCallFactory用于生成PMDCall协议对象
  • PMDCall协议对象为了避免被回收,放到了PMDCallPool里进行管理
  • PMDBaseCall实现了PMDCall协议,完成了主要的网络请求逻辑。分为以下几个步骤

1.callWithRequest准备发起网络请求

2.dealWithInterceptResult遍历所有PMDIntercept,在真正发起网络请求前进行逻辑处理,处理的过程中可以中断。

3.realCallWithRequest真正发起网络请求,这里的实现交给继承类IGCall来实现,具体的实现可以是http,也可以是tcp,在TIP项目中用到了IGNetworkObject去发网路请求。

4.convertResponse将请求回来的数据,遍历PMDConverter进行处理。

  • PMDInterceptor和PMDConverter协议分别是需要在网络请求发出之前和之后要处理的逻辑,只要实现此协议,加入到PMDBaseCall中就可以了。

基本的结构就是这样了,除此之外,框架还实现了取消发送,重新发送等逻辑,这里不再详细介绍。

对iOS App签名的理解

关于iOS设备签名的原理,这篇文章说的比较清楚了。如果忘了的话可以重新阅读以下。

iOS App 签名的原理

这里按照我的理解角度复述一下:

  1. 为了不让每次开发App,都将App上传到苹果后台,用苹果后台的私钥A签名,需要本地生成一对公私钥。这样每次签名就在本地Mac机器上,用私钥L签名就可以了。
  2. 那么苹果如果验证公钥L的合法性呢,就需要开发者将公钥L上传到苹果的后台,苹果用私钥A进行一次签名,签名+公钥L就是证书了。(这里实际上用一次对公钥L的签名替代了每次对App的签名,省去了App每次修改都上传签名的麻烦,很巧妙)
  3. 第2步中的证书+一些额外信息(AppId,设备列表,push权限等)全部都在苹果的后台用私钥A签名,打包成的东西叫Provisioning Profile,下载到本地Mac后,打包在App中。安装在iOS设备的时候,用公钥A对其进行解密,验证证书+一些额外信息(AppId,设备列表,push权限等。证书验证后拿到公钥L,对App数据进行解密验证(App是用私钥L签名的)。

好了,上边的文字其实看着还是有点乱和绕,权当我自己的思路做参考吧。

回到我们的项目,其实有一点是可以改进的。

以上图片是我们项目在苹果后台的证书配置,可以看到,有很多项,原因是每个开发的电脑上都生成了一对公钥L和私钥L,然后将公钥L上传到苹果后台生成了证书。更好的办法是,只用一台开发电脑生成公钥L和私钥L,然后到处p12文件,分享给别的电脑,这样苹果后台就只需要一个证书就可以了。

在新电脑上恢复Octopress

换了新电脑,如何在新电脑上继续使用OctoPress呢,只需要执行以下命令

1.首先将博客的源文件clone到本地的octopress文件夹内

1
$ git clone -b source https://github.com/username/username.github.io.git octopress  

2.将博客文件clone到octopress的_deploy文件夹内

1
2
$ cd octopress  
$ git clone https://github.com/username/username.github.io.git _deploy   

username为github用户名