【问题标题】:Swift init struct with JSON.encode memory leak带有 JSON.encode 内存泄漏的 Swift init 结构
【发布时间】:2020-12-21 07:03:09
【问题描述】:

我正在使用自定义init 方法将JSON 数据解析为Video struct


extension Video: Codable {
    init(dictionary: [String: Any]) throws {
        self = try JSONDecoder().decode(Video.self, from: JSONSerialization.data(withJSONObject: dictionary))
    }
    private enum CodingKeys: String, CodingKey {
        case duration, nsfw, genres, nextVideo, title, video_thumbnail_9x16, onexone_img, video_script, feature_img, sh_heading, tags, alt_content, video_thumbnail_16x9, pub_date, slug, aspect_ratio, _id, interactive, show, cast_crew, srt, sw_more
    }
}

但我可以在仪器Leaks profiler 中看到这个int 导致内存泄漏。

这里有什么问题?

编辑:更多信息

正如有人指出的那样,泄漏可能在其他任何地方,我确实在仪器检查器中看到了带有closure 的获取数据方法。所以这可能是个问题。

这里是网络调用过程和代码中Video对象的创建。

homeVideosDatasource 属性完成了从 API 获取数据的全部工作。

callHomeVideosAPI 是从 viewDidLoad 调用的。 callHomeVideosAPI 首先获取了一个配置 json,它告诉在主屏幕中加载哪些部分。这些部分包含Video 对象(与其他一些对象一起,它们也会导致泄漏)。

    var homeVideosDatasource = HomeVideosDatasource()
override func viewDidLoad() {
        super.viewDidLoad()
        
        NotificationCenter.default.addObserver(self, selector: #selector(updateFirebaseToken), name: Notification.Name(Constants.fcmToken), object: nil)
        
        notificationUpdate()
        
        updateFirebaseToken()
        
        activityIndicatorView.type = .ballPulse
        activityIndicatorView.color = UIColor(hexString: Constants.kPinkColor)
        
        self.refreshControl.tintColor = .white
        self.refreshControl.addTarget(self, action: #selector(callAPIs), for: .valueChanged)
        collectionView.addSubview(refreshControl)
        
        callHomeVideosAPI()
        setupViews()
        
        NotificationCenter.default.post(name: Notification.Name.init(rawValue: Constants.homeLoadedNotification), object: nil)
        if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
            // Tell Appdelegate that app is loaded
            appDelegate.isHomeLoaded = true
            appDelegate.showVideoDetailFromNotification()
        }
    }

    private func callHomeVideosAPI() {
        
        DispatchQueue.global(qos: .background).async {
            self.homeVideosDatasource.fetchDataForHomeVideos { [weak self] (completed, error) in
                
                guard let self = self else { return }
                
                if let error = error {
                    
                    DispatchQueue.main.async {
                        self.view.showMessageTicker(message: error)
                    }
                    return
                }
                
                if completed {
                    // remove sections with no data
                    let homeVideosSectionsWithDataOnly = self.homeVideosDatasource.datasource.filter { (homeVideosSection) -> Bool in
                        if homeVideosSection.datasource.count != 0 {
                            return true
                        } else {
                            return false
                        }
                    }
                    
                    self.homeVideosDatasource.datasource = homeVideosSectionsWithDataOnly
                    
                    self.stopAnimating()
                    self.refreshControl.endRefreshing()
                    self.state = .success
                } else {
                    self.state = .error
                }
            }
        }
    }
    

现在HomeVideosDatasource

class HomeVideosDatasource {
    private var provider = MoyaProvider<ScoopWhoop>(plugins: [CompleteUrlLoggerPlugin()])
    private var scoopWhoopProvider = MoyaProvider<ScoopWhoop>(plugins: [CompleteUrlLoggerPlugin()])
    
    var datasource = [HomeVideosSection]()
    private var dataCount = 0
    
    func fetchDataForHomeVideos(_ closure: @escaping (Bool, String?) -> Void) {
        
        if !(NetworkState().isInternetAvailable) {
            closure(false, Constants.noInternetConnectionString)
            return
        }
        
        DispatchQueue.global(qos: .background).async {
            self.scoopWhoopProvider.request(.home) { [weak self] result in
                guard let self = self else { return }
                
                switch result {
                case .success(let response):
                    do {
                        
                        if var responseDict = try response.mapJSON() as? Dictionary<String, Any> {
                            
                            if let data = responseDict["data"] as? [Dictionary<String, Any>] {
                                
                                self.datasource.removeAll()
                                self.dataCount = data.count
                                
                                for (index, dataDict) in data.enumerated() {
                                    
                                    let homeVideosSection = HomeVideosSection(dataDict)
                                    self.datasource.append(homeVideosSection)
                                    
                                    homeVideosSection.fetchDataForSection(index) { [weak self] (success) in
                                        
                                        guard let self = self else { return }
                                        
                                            self.datasource[index] = homeVideosSection
                                            
                                            let dataNotSetSections = self.datasource.filter { (homeVideosSectionObject) -> Bool in
                                                !homeVideosSectionObject.isDataSet
                                            }
                                            
                                            if dataNotSetSections.count == 0 {
                                                
                                                DispatchQueue.main.async {
                                                    closure(true, nil)
                                                }
                                                
                                            } else {

                                                DispatchQueue.main.async {
                                                    closure(false, nil)
                                                }
                                                
                                            }
                                            
                                        } else {
                                            
                                            DispatchQueue.main.async {
                                                closure(false, nil)
                                            }
                                            
                                        }
                                    }
                                    
                                }
                                
                            } else {
                                
                                DispatchQueue.main.async {
                                    closure(false, nil)
                                }
                                
                            }
                            
                        } else {
                            
                            DispatchQueue.main.async {
                                closure(false, nil)
                            }
                            
                        }
                    } catch (let error) {
                        
                        DispatchQueue.main.async {
                            closure(false, error.localizedDescription)
                        }
                        
                    }
                case .failure(let error):
                    
                    DispatchQueue.main.async {
                        closure(false, error.localizedDescription)
                    }
                    
                }
            }
        }
    }
}

还有HomeVideosSection:

class HomeVideosSection {
    var section_type: String!
    var value: Value!
    var showDetail: ShowDetail?
    var isDataSet = false
    var datasource = [Any]()
    
    private var provider = MoyaProvider<ScoopWhoop>(plugins: [CompleteUrlLoggerPlugin()])
    
    convenience init(_ dictionary: Dictionary<String, Any>) {
        self.init()
        
        section_type = dictionary["section_type"] as? String
        do {
            value = try JSONDecoder().decode(Value.self, from: JSONSerialization.data(withJSONObject: dictionary["value"] as? [String: Any] as Any))
        } catch (let error) {
            print("Error setting section \(section_type ?? "") value : \(error.localizedDescription)")
        }
    }
    
    fileprivate func fetchDataForSection(_ index: Int, closure: @escaping (Bool) -> Void) {
        
        print("fetching detail for section : \(section_type ?? "")")

        // fetch details for section types

        if !(NetworkState().isInternetAvailable) {
            closure(false)
            return
        }
        
        var requestType: ScoopWhoop?
        
        if section_type == "app_exclusive" {
            requestType = .appExclusiveVideos(offset: nil)
        } else if section_type == "recently_added" {
            requestType = .videos(offset: nil)
        } else if section_type == "shows" {
            requestType = .shows(offset: nil)
        } else if section_type == "anchors" {
            requestType = .actors(offset: nil)
        } else if section_type == "more_shows" {
            requestType = .filteredShows(offset: nil, filter_slug: value.slug)
        } else if section_type == "sw_shows_video" {
            requestType = .filteredShows(offset: nil, filter_slug: value.slug, filter_type:"show_sw_more")
        } else if section_type == "sw_videos" {
            requestType = .filteredShows(offset: nil, filter_slug: nil, filter_type:"sw_more")
        } else if section_type == "sw_shows" {
            requestType = .scoopwhoopShows(offset: nil)
        } else if section_type == "trending" {
            requestType = .filteredShows(offset: nil, filter_slug: nil, filter_type:"trending")
        } else if section_type == "most_viewed" {
            requestType = .filteredShows(offset: nil, filter_slug: nil, filter_type:"most_viewed")
        } else {
            // unhandled section_type
            self.isDataSet = true
            
            print("Data set for section : \(self.section_type ?? "")")
            
            closure(true)
        }
        
        DispatchQueue.global(qos: .background).async {
            if let requestType = requestType {
                
                self.provider.request(requestType) { [weak self] result in
                    guard let self = self else { return }
                    
                    switch result {
                    case .success(let response):
                        do {
                            if let responseDict = try response.mapJSON() as? Dictionary<String, Any> {
                                if let data = responseDict["data"] as? [Dictionary<String, Any>] {
                                    
                                    if let showDetails = responseDict["show_details"] as? [String : Any] {
                                        do {
                                            let dataObject = try ShowDetail(dictionary: showDetails)
                                            self.showDetail = dataObject
                                        } catch (let error) {
                                            print("HomeVideoSection ShowDetail object error " + error.localizedDescription)
                                        }
                                    }
                                    
                                    for dataDict in data {
                                        
                                        let dataDict = dataDict
                                        
                                        if self.section_type == "shows" || self.section_type == "sw_shows" {
                                            
                                            do {
                                                let show = try Show(dictionary: dataDict)
                                                self.datasource.append(show)
                                            } catch (let error) {
                                                print("HomeVideoSection Show object error " + error.localizedDescription)
                                            }
                                            
                                        } else if self.section_type == "anchors" {
                                            
                                            do {
                                                let actor = try Anchor(dictionary: dataDict)
                                                self.datasource.append(actor)
                                            } catch (let error) {
                                                print("HomeVideoSection Anchor object error " + error.localizedDescription)
                                            }
                                            
                                        } else {
                                            
                                            do {
                                                let video = try Video(dictionary: dataDict)
                                                self.datasource.append(video)
                                            } catch (let error) {
                                                print("HomeVideoSection Video object error " + error.localizedDescription)
                                            }
                                            
                                        }
                                        
                                    }
                                    
                                    if self.section_type != "anchors" && self.datasource.count != 0 {
                                        self.datasource.append(ViewMore(title: "View All"))
                                    }
                                    
                                    self.isDataSet = true
                                    
                                    print("Data set for section : \(self.section_type ?? "")")
                                    
                                    closure(true)
                                } else {
                                    print("Data set for section dict map error: \(self.section_type ?? "")")
                                    
                                    closure(false)
                                }
                            }
                        } catch {
                            print("Data set for section JSON map error: \(self.section_type ?? "")")
                            
                            closure(false)
                        }
                    case .failure:
                        print("Data set for section failure: \(self.section_type ?? "")")
                    
                        closure(false)
                    }
                }
            }
        }
    }
}

【问题讨论】:

    标签: ios swift memory-leaks instruments


    【解决方案1】:

    问题可能在于您使用init 方法的方式。在 Swift 中,Objective-C 中使用的模式不再适用,这意味着您不应该为 self 分配任何东西。例如,如果某些 init 在 Objective-C 中看起来像这样:

    - (instancetype)init {
        self = [super init];
        if (self) {
            // Initializing code
        }
        return self;
    }
    

    在 Swift 中,这个完全相同的 init 看起来像:

    init() {
        // Initialize all members defined by this subclass
        super.init()
        // Perform other initialization routines (e.g. call self.configure())
    }
    

    请注意,对self 的赋值和return self 语句被一个简单的super.init() 调用替换。需要注意的另一件重要的事情是子类中的所有成员必须在调用 super 之前初始化,其余的初始化逻辑必须在调用 super 之后,这些规则在 Objective-C 中根本不存在。这些是 Swift 强加的规则,以确保初始化器尽可能安全,如果尝试使用与此标准模式不同的东西,可能会产生很多奇怪的副作用。

    现在,回到问题中的问题,除了分配给self 之外,我没有正确解释为什么您的代码会导致内存泄漏是某种未定义的行为。

    最好的替代方法是在静态方法中执行反序列化:

    extension Video: Codable {
    
        static func create(from dictionary: [String: Any]) throws -> Video {
            return try JSONDecoder().decode(Video.self, from: JSONSerialization.data(withJSONObject: dictionary))
        }
    
        private enum CodingKeys: String, CodingKey {
            case duration, nsfw, genres, nextVideo, title, video_thumbnail_9x16, onexone_img, video_script, feature_img, sh_heading, tags, alt_content, video_thumbnail_16x9, pub_date, slug, aspect_ratio, _id, interactive, show, cast_crew, srt, sw_more
        }
    }
    

    更新:

    由于上面使用静态方法的 sn-p 可以正常工作,更好的解决方案是将 JSON 反序列化为对象委托给专门的函数/类:

    func decodeJSON<T: Decodable>(_ dictionary: [String: Any]) throws -> T {
        return try JSONDecoder().decode(T.self, from: JSONSerialization.data(withJSONObject: dictionary))
    }
    

    并让 Swift 类型推断机制来决定 T 是什么类型:

    let json = [String: Any]()
    let video: Video = try? decodeJSON(json)
    

    更新 2

    其余代码乍一看还不错。那里没有明显的保留周期。我建议尝试对每个闭包使用[weak self] 并检查泄漏是否仍然存在(弱捕获自我通常是一个好习惯,除非你特别需要保持对它的强烈引用)。如果这不能消除内存泄漏,那么它可能来自其他地方。请记住,不正确使用 Swift 的内存管理工具会导致非常奇怪的问题。这可能来自一段完全不相关的代码。

    例如,我曾经让应用程序播放随机音频,结果发现它是一个泄露的视图控制器实例,它正在向其他服务(将委托作为强引用)注册一个委托,从而创建了一个强参考周期。整个音频管理都是正确实现的(音频需要在一定的时间间隔播放),而且很难追踪。因此,花了几个小时调试视图控制器和音频服务之间的交互后,我终于发现了泄漏发生的位置,这令人沮丧,以至于它与产生意外行为的代码完全无关。

    所以我想说的是,在你用这个特定的代码部分尝试了更多的东西之后(虽然这对我来说看起来很不错),尝试检查并清理其余的代码,因为有问题出在其他地方的可能性很大。

    【讨论】:

    • 感谢您的解释。我尝试了您的第一个静态方法。但它仍然给我Video.create(from:) 方法的泄漏。我已经像下面这样使用它do { let video = try Video.create(from: dataDict) self.datasource.append(video) } catch (let error) { print("HomeVideoSection Video object error " + error.localizedDescription) }
    • 好吧,那么泄漏很可能来自其他地方。尝试查看分析器中的其他分配并检查 datasource 集合是否被释放。还要非常注意使用闭包。如果您需要进一步的帮助,请更新问题或使用创建这些 Video 对象的代码发布另一个问题,例如您触发网络调用或数据库调用的位置或从中获取它的任何来源。它很可能是一个将所有这些对象保存到内存中的引用循环。
    • 感谢您的回复。我添加了更多代码。请看一下
    猜你喜欢
    • 2016-03-28
    • 2011-10-04
    • 2020-03-19
    • 2011-11-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多