使用JSON的Swift初始化结构编码内存泄漏

问题描述

我正在使用自定义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分析器中看到,这个int导致了内存泄漏。

Profiler Screenshot

这是什么问题?

编辑:更多信息

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

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

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

callHomeVideosAPI调用

viewDidLoadcallHomeVideosAPI首先获取一个配置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_type:"trending")
        } else if section_type == "most_viewed" {
            requestType = .filteredShows(offset: 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)
                    }
                }
            }
        }
    }
}

解决方法

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

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

在Swift中,完全相同的初始化看起来像:

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

请注意,对selfreturn 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
    }
}

更新:

由于上面使用静态方法的代码片段就可以正常工作,因此更好的解决方案是将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的内存管理工具会导致非常奇怪的问题。这可能来自完全不相关的代码。

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

所以我想说的是,在这段特定的代码部分(尽管对我来说看起来还不错)尝试了几件事之后,请尝试检查并清理其余的代码,因为问题出在其他地方的可能性很大。