iOS多线程 - 下载及图片缓存

线程可以理解为下载的通道,一个线程就是一个文件的下载通道,多线程也就是同时开起好几个下载通道.当服务器提供下载服务时,使用下载者是共享带宽的,在优先级相同的情况下,总服务器会对总下载线程进行平均分配。不难理解,如果你线程多的话,那下载的越快。现流行的下载软件都支持多线程。

下面我们通过代码来了解以下几个知识点:

  1. 多线程下载;
  2. 自定义 NSOperation ;
  3. NSCache 增删改查;
  4. NSFileManager 增删改查;
  5. 代理模式;
  6. 闭包反向传值;
  7. 扩展方法;
  8. Plist 文件解析;

首先我们给 String 类型添加扩展方法,获取字符串的MD5值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
extension String {
var md5 : String{
let str = self.cStringUsingEncoding(NSUTF8StringEncoding)
let strLen = CC_LONG(self.lengthOfBytesUsingEncoding(NSUTF8StringEncoding))
let digestLen = Int(CC_MD5_DIGEST_LENGTH)
let result = UnsafeMutablePointer<CUnsignedChar>.alloc(digestLen);
CC_MD5(str!, strLen, result);
let hash = NSMutableString();
for i in 0 ..< digestLen {
hash.appendFormat("%02x", result[i]);
}
result.destroy();
return String(format: hash as String)
}
}

下面我们来自定义一个 NSOperation ,新建一个继承自 NSOperation 的类,并声明以下属性:

1
2
3
4
5
6
7
8
9
10
class DownloadOperation: NSOperation {
var memCache: NSCache!
var fileMgr: NSFileManager!
var diskCachePath: String!
var operations: [NSOperation] = []
var getImageQueue: NSOperationQueue!
}

然后声明一个闭包:

1
typealias Block = (UIImage) -> (Void)

最后声明一个协议,并创建两个代理方法:

1
2
3
4
5
6
protocol DownloadDelegate {
func userCache (operation: DownloadOperation, imageCache:NSCache)
func cleanCache (operation: DownloadOperation, imageCache:NSCache)
}

下面我们重写自定义 NSOperation 的 init 方法,用来创建沙盒目录,实例 NSCache NSFileManager NSOperationQueue :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
override init() {
memCache = NSCache()
fileMgr = NSFileManager()
let paths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory,
NSSearchPathDomainMask.UserDomainMask,
true)
self.diskCachePath = paths.first?.stringByAppendingString("/ImageCache")
if !(NSFileManager.defaultManager().fileExistsAtPath(self.diskCachePath!)) {
try! NSFileManager.defaultManager().createDirectoryAtPath(self.diskCachePath!,
withIntermediateDirectories: true,
attributes: nil)
}
getImageQueue = NSOperationQueue()
}

下面是闭包反向传值的第一步,初始化闭包:

1
2
3
func ininWithblock(block: Block) {
showImage = block
}

下面我们利用文件名获得要保存的路径:

1
2
3
4
5
func diskPathForKey(key: String) -> String {
return (diskCachePath?.stringByAppendingString("/\(key.md5)"))!
}

下面我们利用文件名判断这个文件在内存中是否存在:

1
2
3
4
5
6
7
8
9
10
11
func cacheIsExist(key: String) -> Bool {
let data = memCache.objectForKey(key)
if data != nil {
return true
}
else {
return false
}
}

下面我们利用文件名判断这个文件在磁盘中是否存在:

1
2
3
4
5
func fileIsExist(key: String) -> Bool {
return fileMgr.fileExistsAtPath(diskPathForKey(key))
}

下面我们实现利用文件名从内存中获取文件:

1
2
3
4
5
6
func readFromeMem(key: String) -> UIImage {
let image = memCache.objectForKey(key) as! UIImage
return image
}

下面我们实现利用文件名从磁盘中获取文件:

1
2
3
4
5
6
func readFromeDisk(key: String) -> UIImage {
let data = fileMgr.contentsAtPath(diskPathForKey(key))
let image = UIImage(data: data!)
return image!
}

下面我们实现利用链接下载文件:

1
2
3
4
5
6
7
8
func download(key: String) -> UIImage {
let url = NSURL(string: key)
let data = NSData(contentsOfURL: url!)
let image = UIImage(data: data!)
return image!
}

下面实现将文件写入内存:

1
2
3
4
5
func saveToMem(image: UIImage, key: String) {
memCache.setObject(image, forKey: key)
}

下载实现将文件写入磁盘:

1
2
3
4
5
6
7
8
9
func saveToDisk(image: UIImage, key: String) {
let key = diskPathForKey(key)
fileMgr.createFileAtPath(key,
contents: UIImageJPEGRepresentation(image, 1.0),
attributes: nil)
}

下面实现利用链接下载文件,并将下载的文件写入内存和磁盘:

1
2
3
4
5
6
7
8
9
10
func saveToMemAndDisk(key: String, toDisk: Bool) {
let image = download(key)
saveToMem(image, key: key)
if toDisk {
saveToDisk(image, key: key)
}
}

下面实现读取文件,如果内存中有就从内存读取,如果磁盘中有就从磁盘读取,并写入到内存,如果都没有,就下载并写入到内存和磁盘:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func readImage(key: String) -> UIImage {
if cacheIsExist(key) {
let image = readFromeMem(key)
return image
}
else if fileIsExist(key) {
let image = readFromeDisk(key)
saveToMem(image, key: key)
return image
}
saveToMemAndDisk(key, toDisk: true)
return readFromeMem(key)
}

下面实现利用存放链接的数组,批量下载文件,设置下载线程的最大值并使用 block 执行 ViewController 预设的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func getImage(keys: [String], max: Int) {
for key in keys {
let operation = NSBlockOperation(block: {
let image = self.readImage(key)
self.showImage!(image)
})
operations.append(operation)
}
getImageQueue.maxConcurrentOperationCount = max
getImageQueue.addOperations(operations, waitUntilFinished: true)
}

下面实现利用文件名删除文件:

1
2
3
4
5
func deleteFile(key: String) {
if fileMgr.isDeletableFileAtPath(key) {
print("删除成功")
}

下面实现调用代理的两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func showImg() {
if (self.delegete != nil) && (self.delegete?.userCache != nil) {
self.delegete?.userCache(self, imageCache: self.memCache)
}
}
func deleteImg() {
if (self.delegete != nil) && (self.delegete?.cleanCache != nil) {
self.delegete?.cleanCache(self, imageCache: self.memCache)
}
}

下面我们回到 ViewController 中,声明以下几个属性:

1
2
3
4
5
6
7
8
9
10
class ViewController: UIViewController {
var keys: NSArray!
var imgView: UIImageView!
var downloadBtn: UIButton?
var cleanBtn: UIButton?
var operation: DownloadOperation!
}

下面我们创建 block 要执行的方法:

1
2
3
4
5
6
7
func showImage(image: UIImage) {
self.imgView = UIImageView(frame: self.view.bounds)
self.view.addSubview(self.imgView!)
self.imgView.image = image
print("block 加载图片")
}

下面我们解析 Plist 文件为数组,并将包含下载链接的数组传给自定义 NSOperation 的获取文件方法:

1
2
3
4
5
6
7
8
func downloadForKeys(max: Int) {
let imgPaths = NSBundle.mainBundle().pathForResource("ImageLinks", ofType: "plist")
let imgUrls = NSURL(fileURLWithPath: imgPaths!)
self.keys = NSArray(contentsOfURL: imgUrls)!
operation.getImage(keys as! [String], max: max)
}

下面我们给 ViewController 添加两个 Button :

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
func addButton() {
let width = self.view.bounds.size.width
let height = self.view.bounds.size.height
self.downloadBtn = UIButton(type: .Custom)
let downloadBtnframe = CGRectMake(20, height-60, 80, 40)
self.downloadBtn?.frame = downloadBtnframe
self.downloadBtn?.backgroundColor = UIColor.lightGrayColor()
self.downloadBtn?.titleLabel?.font = UIFont.systemFontOfSize(18)
self.downloadBtn?.setTitle("下载", forState: .Normal)
self.cleanBtn = UIButton(type: .Custom)
let cleanBtnframe = CGRectMake(width-80-20, height-60, 80, 40)
self.cleanBtn?.frame = cleanBtnframe
self.cleanBtn?.backgroundColor = UIColor.lightGrayColor()
self.cleanBtn?.titleLabel?.font = UIFont.systemFontOfSize(18)
self.cleanBtn?.setTitle("清除", forState: .Normal)
self.downloadBtn?.addTarget(self,
action: #selector(ViewController.downloadBtnClicked),
forControlEvents: .TouchUpInside)
self.cleanBtn?.addTarget(self,
action: #selector(ViewController.cleanBtnClicked),
forControlEvents: .TouchUpInside)
self.view.addSubview(self.downloadBtn!)
self.view.addSubview(self.cleanBtn!)
}

下面我们实现 Button 的响应事件方法:

1
2
3
4
5
6
7
8
9
10
11
func downloadBtnClicked() {
operation.showImg()
addButton()
}
func cleanBtnClicked() {
operation.deleteImg()
}

下面我们实现代理方法:

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
func userCache(operation: DownloadOperation, imageCache: NSCache) {
imgView.removeFromSuperview()
let width = self.view.bounds.size.width
let imgHeight = self.view.bounds.size.height / CGFloat(Float(keys.count))
for i in 0 ..< keys.count {
let frame = CGRectMake(0, CGFloat(i) * imgHeight, width, imgHeight)
let imageView = UIImageView(frame: frame)
let key = keys[i] as! String
let image = operation.readImage(key)
imageView.image = image
imageView.contentMode = .ScaleToFill
self.view.addSubview(imageView)
}
}
func cleanCache (operation: DownloadOperation, imageCache:NSCache) {
imageCache.removeAllObjects()
for subView in self.view.subviews {
subView.removeFromSuperview()
}
for key in keys {
let temKey = operation.diskPathForKey(key as! String)
operation.deleteFile(temKey)
}
addButton()
}

最后我们引入代理协议,实例化自定义的 ,实例化代理对象,实例化 block,以上几个功能点就完成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ViewController: UIViewController, DownloadDelegate {
var keys: NSArray!
var imgView: UIImageView!
var downloadBtn: UIButton?
var cleanBtn: UIButton?
var operation: DownloadOperation!
override func viewDidLoad() {
super.viewDidLoad()
operation = DownloadOperation()
operation.ininWithblock(showImage)
operation.delegete = self
downloadForKeys(3)
addButton()
}
}

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
extension String {
var md5 : String{
let str = self.cStringUsingEncoding(NSUTF8StringEncoding)
let strLen = CC_LONG(self.lengthOfBytesUsingEncoding(NSUTF8StringEncoding))
let digestLen = Int(CC_MD5_DIGEST_LENGTH)
let result = UnsafeMutablePointer<CUnsignedChar>.alloc(digestLen);
CC_MD5(str!, strLen, result);
let hash = NSMutableString();
for i in 0 ..< digestLen {
hash.appendFormat("%02x", result[i]);
}
result.destroy();
return String(format: hash as String)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
typealias Block = (UIImage) -> (Void)
protocol DownloadDelegate {
func userCache (operation: DownloadOperation, imageCache:NSCache)
func cleanCache (operation: DownloadOperation, imageCache:NSCache)
}
class DownloadOperation: NSOperation {
var memCache: NSCache!
var fileMgr: NSFileManager!
var diskCachePath: String!
var operations: [NSOperation] = []
var getImageQueue: NSOperationQueue!
var showImage: Block?
var delegete:DownloadDelegate?
override init() {
memCache = NSCache()
fileMgr = NSFileManager()
let paths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory,
NSSearchPathDomainMask.UserDomainMask,
true)
self.diskCachePath = paths.first?.stringByAppendingString("/ImageCache")
if !(NSFileManager.defaultManager().fileExistsAtPath(self.diskCachePath!)) {
try! NSFileManager.defaultManager().createDirectoryAtPath(self.diskCachePath!,
withIntermediateDirectories: true,
attributes: nil)
}
getImageQueue = NSOperationQueue()
}
func ininWithblock(block: Block) {
showImage = block
}
func diskPathForKey(key: String) -> String {
return (diskCachePath?.stringByAppendingString("/\(key.md5)"))!
}
func fileIsExist(key: String) -> Bool {
return fileMgr.fileExistsAtPath(diskPathForKey(key))
}
func cacheIsExist(key: String) -> Bool {
let data = memCache.objectForKey(key)
if data != nil {
return true
}
else {
return false
}
}
func readFromeMem(key: String) -> UIImage {
let image = memCache.objectForKey(key) as! UIImage
print("内存读取")
return image
}
func readFromeDisk(key: String) -> UIImage {
let data = fileMgr.contentsAtPath(diskPathForKey(key))
let image = UIImage(data: data!)
print("文件读取")
return image!
}
func deleteFile(key: String) {
if fileMgr.isDeletableFileAtPath(key) {
print("删除成功")
}
}
func download(key: String) -> UIImage {
let url = NSURL(string: key)
let data = NSData(contentsOfURL: url!)
let image = UIImage(data: data!)
print("下载")
return image!
}
func saveToMemAndDisk(key: String, toDisk: Bool) {
let image = download(key)
saveToMem(image, key: key)
if toDisk {
saveToDisk(image, key: key)
}
}
func saveToMem(image: UIImage, key: String) {
memCache.setObject(image, forKey: key)
print("写入内存")
}
func saveToDisk(image: UIImage, key: String) {
let key = diskPathForKey(key)
fileMgr.createFileAtPath(key,
contents: UIImageJPEGRepresentation(image, 1.0),
attributes: nil)
print("写入文件")
}
func readImage(key: String) -> UIImage {
if cacheIsExist(key) {
let image = readFromeMem(key)
return image
}
else if fileIsExist(key) {
let image = readFromeDisk(key)
saveToMem(image, key: key)
return image
}
saveToMemAndDisk(key, toDisk: true)
return readFromeMem(key)
}
func getImage(keys: [String], max: Int) {
for key in keys {
let operation = NSBlockOperation(block: {
let image = self.readImage(key)
self.showImage!(image)
})
operations.append(operation)
}
getImageQueue.maxConcurrentOperationCount = max
getImageQueue.addOperations(operations, waitUntilFinished: true)
}
func showImg() {
if (self.delegete != nil) && (self.delegete?.userCache != nil) {
self.delegete?.userCache(self, imageCache: self.memCache)
}
}
func deleteImg() {
if (self.delegete != nil) && (self.delegete?.cleanCache != nil) {
self.delegete?.cleanCache(self, imageCache: self.memCache)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
class ViewController: UIViewController, DownloadDelegate {
var keys: NSArray!
var imgView: UIImageView!
var downloadBtn: UIButton?
var cleanBtn: UIButton?
var operation: DownloadOperation!
override func viewDidLoad() {
super.viewDidLoad()
operation = DownloadOperation()
operation.ininWithblock(showImage)
operation.delegete = self
downloadForKeys(3)
addButton()
}
func downloadForKeys(max: Int) {
let imgPaths = NSBundle.mainBundle().pathForResource("ImageLinks", ofType: "plist")
let imgUrls = NSURL(fileURLWithPath: imgPaths!)
self.keys = NSArray(contentsOfURL: imgUrls)!
operation.getImage(keys as! [String], max: max)
}
func showImage(image: UIImage) {
self.imgView = UIImageView(frame: self.view.bounds)
self.view.addSubview(self.imgView!)
self.imgView.image = image
print("block 加载图片")
}
func addButton() {
let width = self.view.bounds.size.width
let height = self.view.bounds.size.height
self.downloadBtn = UIButton(type: .Custom)
let downloadBtnframe = CGRectMake(20, height-60, 80, 40)
self.downloadBtn?.frame = downloadBtnframe
self.downloadBtn?.backgroundColor = UIColor.lightGrayColor()
self.downloadBtn?.titleLabel?.font = UIFont.systemFontOfSize(18)
self.downloadBtn?.setTitle("下载", forState: .Normal)
self.cleanBtn = UIButton(type: .Custom)
let cleanBtnframe = CGRectMake(width-80-20, height-60, 80, 40)
self.cleanBtn?.frame = cleanBtnframe
self.cleanBtn?.backgroundColor = UIColor.lightGrayColor()
self.cleanBtn?.titleLabel?.font = UIFont.systemFontOfSize(18)
self.cleanBtn?.setTitle("清除", forState: .Normal)
self.downloadBtn?.addTarget(self,
action: #selector(ViewController.downloadBtnClicked),
forControlEvents: .TouchUpInside)
self.cleanBtn?.addTarget(self,
action: #selector(ViewController.cleanBtnClicked),
forControlEvents: .TouchUpInside)
self.view.addSubview(self.downloadBtn!)
self.view.addSubview(self.cleanBtn!)
}
func userCache(operation: DownloadOperation, imageCache: NSCache) {
imgView.removeFromSuperview()
let width = self.view.bounds.size.width
let imgHeight = self.view.bounds.size.height / CGFloat(Float(keys.count))
for i in 0 ..< keys.count {
let frame = CGRectMake(0, CGFloat(i) * imgHeight, width, imgHeight)
let imageView = UIImageView(frame: frame)
let key = keys[i] as! String
let image = operation.readImage(key)
imageView.image = image
imageView.contentMode = .ScaleToFill
self.view.addSubview(imageView)
}
}
func cleanCache (operation: DownloadOperation, imageCache:NSCache) {
imageCache.removeAllObjects()
for subView in self.view.subviews {
subView.removeFromSuperview()
}
for key in keys {
let temKey = operation.diskPathForKey(key as! String)
operation.deleteFile(temKey)
}
addButton()
}
func downloadBtnClicked() {
operation.showImg()
addButton()
}
func cleanBtnClicked() {
operation.deleteImg()
}
}