iOS NSCache内存优化

       做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方式。

iOS NSCache内存优化_第1张图片

      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


     UITableView/UICollectionView内存优化的套路就包括将图片保存到NSCache中, 每次先从NSCache中取,如果查不到再执行上面的UIImage构造方法。

测试代码:

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
    }

}




你可能感兴趣的:(iOS)