做Java语言开发的都知道LruCache类, iOS与其对应的就是NSCache, 它俩功能类似但实现原理不同。 在Xcode API Reference里有NSCache类的详细介绍, 简要介绍一下:
1、NSCache是苹果提供的缓存类,主流的三方图片框架都用它,例如OC版的SDWebImage和Swift版的Kingfisher。
2、NSCache在系统内存低时,会自动释放一些对象(有点像Android的弱引用);但实际编码过程中,推荐在低内存的警告函数里主动释放NSCache对象。
3、NSCache自动释放内存的算法是不确定的, 有时是按照LRU释放,有时随机释放;算是个坑,要重点关注!!!
4、NSCache的API是线程安全的, 不用担心多线程并发问题;
5、NSCache是Key-Value数据结构,其中key是强引用,不实现NSCoping协议。
NSCache类重要属性:
1、countLimit: 就是缓存对象的总数,默认值是0,即不限制;
2、totalCostLimit: 缓存对象的总大小,超出上限时会先释放足够的内存空间出来再执行插入操作;
3、delegate协议属性:缓存的value对象可以实现该NSDiscardableContent
协议,当NSCache要释放对象时会执行回调函数,在回调函数里释放value对象申请的其它资源。
场景: Android ListView的item有图片,如果不做处理,滑动列表时内存会一直增长(大多数app都用了图片库,但忽视了其内存回收的原理。简要说就是注册ListView的recycleListener或者重载控件的onDetach函数,控件释放图片的引用)。
那么iOS会不会出现类似的问题呢? 写个tableview,10000条记录,每条记录都是1个图片和1行文字, 快速滑动列表并观察内存变化, 发现内存并没增长!!!
这里要注意获取UIImage对象时的区别:
1、使用named方式iOS会将图片缓存到内存, 以文件名为关键字在内存缓存中查,查不到时才从资源里读文件;优点是频繁访问时速度快,缺点是吃内存;
2、使用contensOfFile方式就是每次都从资源里读文件,不做内存缓存;
//let image = UIImage(named: "image1") //iOS会将图片缓存到内存,下次先通过名字从内存查
let path = Bundle.main.path(forResource: "image1", ofType: "png" )
let image = UIImage(contentsOfFile: path!) //不缓存,每次都读文件和解码
因为是要验证是否内存增长,所以在demo中使用contentsOfFile方式。
demo中扩展了UIImage类并实现NSDiscardableContent协议, 查询滑动列表时的日志发现。 NSCache在释放value对象时,会执行value的discardContentIfPossible, 从名字就可以看出该回调函数的作用是释放value对象申请的资源。
cache release: <_TtCC12NSCacheTable14ViewController8ImageExt: 0x7c8391f0>, {560, 398}
discardContentIfPossible: 1
cache release: <_TtCC12NSCacheTable14ViewController8ImageExt: 0x7c8de1c0>, {560, 398}
discardContentIfPossible: 1
cache release: <_TtCC12NSCacheTable14ViewController8ImageExt: 0x7c734ba0>, {560, 398}
discardContentIfPossible: 1
测试代码:
class ViewController: UIViewController, UITableViewDataSource, NSCacheDelegate {
var tablewView: UITableView?
let CELL_IDENTIFIER = "cell_image"
var cache: NSCache?
//有子对象需要释放资源时可以实现NSDiscardableContent接口
class ImageExt: UIImage, NSDiscardableContent {
var count = 1
var isReleased = false
//主动调用,做逻辑判断。 NSCache不会调用
public func beginContentAccess() -> Bool {
print("beginContentAccess: \(count)")
count += 1
return true
}
//主动调用,做逻辑判断。 NSCache不会调用
public func endContentAccess() {
print("endContentAccess: \(count)")
count -= 1
}
//NSCache类会回调执行该方法
public func discardContentIfPossible() {
print("discardContentIfPossible: \(count)")
if count == 0 && !isReleased {
//.....执行释放流程
isReleased = true
} else {
isReleased = false
}
}
//主动调用,做逻辑判断。 NSCache不会调用
public func isContentDiscarded() -> Bool {
print("isContentDiscarded: \(count)")
return isReleased
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
tablewView = UITableView(frame: self.view.frame)
tablewView?.backgroundColor = UIColor.white
tablewView?.dataSource = self
tablewView?.register(ImageTableViewCell.self, forCellReuseIdentifier: CELL_IDENTIFIER)
tablewView?.estimatedRowHeight = 150
tablewView?.rowHeight = UITableViewAutomaticDimension
tablewView?.separatorColor = UIColor.gray
tablewView?.separatorStyle = .singleLine
tablewView?.tableFooterView = UIView() //解决空白横线问题
self.view.addSubview(tablewView!)
cache = NSCache()
cache?.countLimit = 10 //最多10个
cache?.totalCostLimit = 50 * 1024 * 1024 //最多缓存50M字节
cache?.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
cache?.removeAllObjects() //释放内存
}
public func cache(_ cache: NSCache, willEvictObject obj: Any) {
print("cache release: \(obj)")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 100000
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: CELL_IDENTIFIER, for: indexPath)
as? ImageTableViewCell
if (cell == nil) {
cell = ImageTableViewCell(style: .default, reuseIdentifier: CELL_IDENTIFIER)
}
//说明:iOS跟Android的listview机制不同,iOS可以自动释放无效item的内存!!!!!!
//let image = UIImage(named: "image1") //iOS会将图片缓存到内存,下次先通过名字从内存查
let path = Bundle.main.path(forResource: "image1", ofType: "png" )
let image = UIImage(contentsOfFile: path!) //不缓存,每次都读文件和解码
let imageExt = ImageExt(contentsOfFile: path!)
cache?.setObject(imageExt!, forKey: "\(indexPath.row)" as NSString, cost: 1 * 1024 * 1024) //模拟1M字节
//cache?.setObject(image!, forKey: "\(indexPath.row)" as NSString, cost: 1 * 1024 * 1024) //模拟5M字节
//cell?.icon?.image = cache?.object(forKey: "\(indexPath.row)" as NSString) as! UIImage
cell?.icon?.image = image
return cell!
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
}
class ImageTableViewCell: UITableViewCell {
var icon: UIImageView?
var name: UILabel?
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
initViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/**
* 初始化控件
* 注意: snapkit的offset函数在top/left使用正数、right/bottom使用负数!
*/
func initViews() {
icon = UIImageView()
icon?.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
//let image = UIImage(named: "image_1") //缓存起来,下次读取先从内存查
//let path = Bundle.main.path(forResource: "image1", ofType: "png")
//let image = UIImage(contentsOfFile: path!) //不缓存,每次都重新加载
//icon?.image = image
self.contentView.addSubview(icon!)
icon?.snp.makeConstraints{ make in
make.width.height.equalTo(50)
make.top.equalTo(self.contentView).offset(10)
make.left.equalTo(self.contentView.snp.left).offset(5)
make.centerY.equalTo(self.contentView)
make.bottom.equalTo(self.contentView).offset(-10) //这是是负数-10
}
name = UILabel()
name?.font = UIFont.systemFont(ofSize: 16)
name?.textColor = .black
name?.text = "测试死啦地方;少量的看风景;的说法快速的"
name?.adjustsFontSizeToFitWidth = true
self.contentView.addSubview(name!)
name?.snp.makeConstraints{ make in
make.centerY.equalTo(self.contentView)
make.left.equalTo(icon!.snp.right).offset(10)
make.right.equalTo(self.contentView).offset(-10) //负数!
}
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}