代码之家  ›  专栏  ›  技术社区  ›  Ahmed Zaidan

太多的网页视图实例导致手机过热Swift

  •  1
  • Ahmed Zaidan  · 技术社区  · 2 年前

    你好,我正在使用YouTube短片制作一个TikTok克隆。我在一个垂直的选项卡视图中显示视频,允许用户滚动浏览视频列表。由于这些视频在网络上,我使用网络视图来渲染它们。当用户滚动浏览选项卡视图时,会为新视频创建新的网络视图实例。当用户向后滚动时,他们可以在相同的持续时间内看到以前的视频(已经渲染)。这意味着,当用户从网页视图上滑动时,网页视图不会被破坏。滚动几分钟后,设备明显变热,因为许多web视图实例需要大量资源。当用户超过2个视频时,我如何才能破坏这些网络视图。

    import SwiftUI
    import WebKit
    import UIKit
    
    struct AllVideoView: View {
        @State private var selected = ""
    
    
        @State private var arr = ["-q6-DxWZnlQ", "Bp3iu47RRJQ", "lXJdgDjw1Ks", "It3ecCpuzgc", "7WNJjr8QM1w", "z2t0W8YSzZo", "w8RBGoH_6BM", "DJNAUBoxW5g", "Gv0X34FZ_8M", "EUTsaD1JFZE",
        "yM9iLvOL2v4", "lnqhfn2n-Jo", "qkUpWwUAFPA", "Uz21KTMGwAI", "682rP7VrMUI",
        "4AOcYT6tnsE", "DEz9ngMqVT0", "VOY2MviU5ig", "F8DvoxgP77M", "LGiRWOawMiw",
        "Ub8j6l35VEM", "0xEQbJxR2hw", "SVow553Lluc", "0cPTM7v0vlw", "G12vO9ziK0k"]
    
    
        var body: some View {
            ZStack {
                Color.black.edgesIgnoringSafeArea([.bottom, .top])
                TabView(selection: $selected){
                    ForEach(arr, id: \.self){ id in
                        SingleVideoView(link: id).tag(id)
                    }
                    .rotationEffect(.init(degrees: -90))
                    .frame(width: widthOrHeight(width: true), height: widthOrHeight(width: false))
                }
                .offset(x: -10.5) 
                .frame(width: widthOrHeight(width: false), height: widthOrHeight(width: true))
                .rotationEffect(.init(degrees: 90))
                .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
            }
        }
    }
    
    struct SingleVideoView: View {
        let link: String
        @State private var viewIsShowing = false
        @State private var isVideoPlaying = false
        var body: some View {
            ZStack {
                Color.black
                
                SmartReelView(link: link, isPlaying: $isVideoPlaying, viewIsShowing: $viewIsShowing)
    
                Button("", action: {}).disabled(true)
                
                Color.gray.opacity(0.001)
                    .onTapGesture {
                        isVideoPlaying.toggle()
                    }
                
            }
            .ignoresSafeArea()
            .onDisappear {
                isVideoPlaying = false
                viewIsShowing = false
            }
            .onAppear {
                viewIsShowing = true
                isVideoPlaying = true
            }
        }
    }
    
    struct SmartReelView: UIViewRepresentable {
        let link: String
        @Binding var isPlaying: Bool
        @Binding var viewIsShowing: Bool
        
        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }
        
        func makeUIView(context: Context) -> WKWebView {
            let webConfiguration = WKWebViewConfiguration()
            webConfiguration.allowsInlineMediaPlayback = true
            let webView = WKWebView(frame: .zero, configuration: webConfiguration)
            webView.navigationDelegate = context.coordinator
    
            let userContentController = WKUserContentController()
            
            webView.configuration.userContentController = userContentController
    
            loadInitialContent(in: webView)
            
            return webView
        }
    
        func updateUIView(_ uiView: WKWebView, context: Context) {
            var jsString = """
                    isPlaying = \((isPlaying) ? "true" : "false");
                    watchPlayingState();
                """
            uiView.evaluateJavaScript(jsString, completionHandler: nil)
        }
        
        class Coordinator: NSObject, WKNavigationDelegate {
            var parent: SmartReelView
    
            init(_ parent: SmartReelView) {
                self.parent = parent
            }
            
            func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
                if self.parent.viewIsShowing {
                    webView.evaluateJavaScript("clickReady()", completionHandler: nil)
                }
            }
        }
        
        private func loadInitialContent(in webView: WKWebView) {
            let embedHTML = """
            <style>
                body {
                    margin: 0;
                    background-color: black;
                }
                .iframe-container iframe {
                    top: 0;
                    left: 0;
                    width: 100%;
                    height: 100%;
                }
            </style>
            <div class="iframe-container">
                <div id="player"></div>
            </div>
            <script>
                var tag = document.createElement('script');
                tag.src = "https://www.youtube.com/iframe_api";
                var firstScriptTag = document.getElementsByTagName('script')[0];
                firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
    
                var player;
                var isPlaying = false;
                function onYouTubeIframeAPIReady() {
                    player = new YT.Player('player', {
                        width: '100%',
                        videoId: '\(link)',
                        playerVars: { 'playsinline': 1, 'controls': 0},
                        events: {
                            'onStateChange': function(event) {
                                if (event.data === YT.PlayerState.ENDED) {
                                    player.seekTo(0);
                                    player.playVideo();
                                }
                            }
                        }
                    });
                }
            
                function clickReady() {
                    player.playVideo();
                }
                
                function watchPlayingState() {
                    if (isPlaying) {
                        player.playVideo();
                    } else {
                        player.pauseVideo();
                    }
                }
            
            </script>
            """
            
            webView.scrollView.isScrollEnabled = false
            webView.loadHTMLString(embedHTML, baseURL: nil)
        }
    }
    
    func widthOrHeight(width: Bool) -> CGFloat {
        let scenes = UIApplication.shared.connectedScenes
        let windowScene = scenes.first as? UIWindowScene
        let window = windowScene?.windows.first
        
        if width {
            return window?.screen.bounds.width ?? 0
        } else {
            return window?.screen.bounds.height ?? 0
        }
    }
    

    更新的代码

    struct SingleVideoView: View {
        let link: String
        @State private var isVideoPlaying = false
        @State private var destroy = false
    
        @EnvironmentObject var viewModel: VideoModel
    
        var body: some View {
            ZStack {
                SmartReelView(link: link, isPlaying: $isVideoPlaying, destroy: $destroy)
                
                Color.gray.opacity(0.001)
                    .onTapGesture {
                        isVideoPlaying.toggle()
                    }
            }
            .onDisappear {
                isVideoPlaying = false
            }
            .onAppear {
                if viewModel.selected == link {
                    isVideoPlaying = true
                    destroy = false
                }
            }
            .onChange(of: viewModel.selected, perform: { _ in
                if viewModel.selected != link {
                    isVideoPlaying = false
                    
                    if let x = viewModel.VideosToShow.firstIndex(where: { $0.videoID == viewModel.selected }), let j = viewModel.VideosToShow.firstIndex(where: { $0.videoID == link }){
                        if (x - j) > 2 && !destroy {
                            destroy = true
                            print("destroy \(j)")
                        }
                    }
                }
            })
        }
    }
    
    struct SmartReelView: UIViewRepresentable {
        let link: String
        @Binding var isPlaying: Bool
        @Binding var destroy: Bool
        
        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }
        
        func makeUIView(context: Context) -> WKWebView {
            let webConfiguration = WKWebViewConfiguration()
            webConfiguration.allowsInlineMediaPlayback = true
            let webView = WKWebView(frame: .zero, configuration: webConfiguration)
            webView.navigationDelegate = context.coordinator
    
            let userContentController = WKUserContentController()
            
            webView.configuration.userContentController = userContentController
    
            loadInitialContent(in: webView)
            
            return webView
        }
        
        func createView(context: Context) { //copy of makeUIView but doesnt return a webview
            let webConfiguration = WKWebViewConfiguration()
            webConfiguration.allowsInlineMediaPlayback = true
            let webView = WKWebView(frame: .zero, configuration: webConfiguration)
            webView.navigationDelegate = context.coordinator
            let userContentController = WKUserContentController()
            webView.configuration.userContentController = userContentController
            loadInitialContent(in: webView)
        }
    
        func updateUIView(_ uiView: WKWebView, context: Context) {
            
            if destroy && uiView.navigationDelegate != nil {
                destroyWebView(uiView)
            } else if uiView.navigationDelegate == nil {
                createView(context: context)
            }
            
            //rest of code
        }
        
        private func destroyWebView(_ webView: WKWebView) {
            print("destroyed")
            webView.navigationDelegate = nil
            webView.stopLoading()
            webView.removeFromSuperview()
        }
        
        class Coordinator: NSObject, WKNavigationDelegate {
            var parent: SmartReelView
    
            init(_ parent: SmartReelView) {
                self.parent = parent
            }
            
            func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
                //rest of code
            }
        }
        
        private func loadInitialContent(in webView: WKWebView) {
            let embedHTML = """
                //unchanged
            """
            
            webView.scrollView.isScrollEnabled = false
            webView.loadHTMLString(embedHTML, baseURL: nil)
        }
    }
    
    1 回复  |  直到 2 年前
        1
  •  2
  •   VonC    2 年前

    为了优化,您可以考虑使用视图回收机制(“视图池”),以便在每次显示新视频时重用web视图实例,而不是创建新实例。

    然而,由于您特别询问当用户超过2个视频时如何销毁网络视图,因此您可以实现手动取消分配这些网络视图并清除其内容的逻辑。

    手动销毁 WKWebView ,您需要:

    • 从其超级视图中删除web视图(如果有)。
    • 设置 navigationDelegate UIDelegate nil
    • 致电 stopLoading 方法。
    • 将web视图本身设置为 (如果web视图没有强引用,则通常由ARC处理)。

    首先,在中添加标志 SmartReelView 要检查web视图是否处于活动状态,请执行以下操作:

    @Binding var isActive: Bool
    

    更新 updateUIView makeUIView 考虑活动状态的方法:

    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()
        // existing code
        if isActive {
            loadInitialContent(in: webView)
        }
        return webView
    }
    
    func updateUIView(_ uiView: WKWebView, context: Context) {
        if isActive {
            // existing code
        } else {
            destroyWebView(uiView)
        }
    }
    
    private func destroyWebView(_ webView: WKWebView) {
        webView.navigationDelegate = nil
        webView.stopLoading()
        webView.removeFromSuperview()
    }
    

    然后,在 SingleVideoView ,引入逻辑以更新 isActive 基于用户滚动的距离的绑定。您可能需要传递索引并计算视图是否在当前活动视频的2个视频内:

    struct SingleVideoView: View {
        // existing properties
        @Binding var activeIndex: Int // The index of the currently active (visible) video
        let index: Int  // The index of this particular video
    
        var body: some View {
            // existing code
    
            SmartReelView(link: link, isPlaying: $isVideoPlaying, viewIsShowing: $viewIsShowing, isActive: .constant(shouldActivate))
        }
    
        private var shouldActivate: Bool {
            return abs(activeIndex - index) <= 2
        }
    }
    

    在里面 AllVideoView ,保持当前活动视频索引的状态:

    @State private var activeIndex = 0
    

    将此索引传递给每个 单个视频视图 :

    ForEach(arr.indices, id: \.self) { index in
        SingleVideoView(link: arr[index], activeIndex: $activeIndex, index: index)
    }
    

    最后,更新 activeIndex 每当 TabView 的选择更改。

    这些更改应将内存中的网络视图数量限制为仅在当前活动视频的2个视频内的视图,这应能缓解资源问题。

    在SingleVideoView中更改isActive的值并不总是触发updateUiView,尤其是当视图不在屏幕上时。如果我在视图的onChange中放入一个打印,那么每当视频有2个索引时,就会运行这个打印。但是在destroyWebView中添加打印并不总是运行的。这意味着 updateUiView 如果视图未显示,func可能不会检查更改。如果我滚动两个视频,然后滚动回视频,则会打印“已删除”。

    有没有办法从SingleVideoView中保存实例SmartReelView,并直接调用destroyWebView以确保其运行?

    updateUI视图 没有被一致调用应该是由于SwiftUI的优化;它不会更新当前不在屏幕上的视图。由于您正在使用 选项卡视图 ,SwiftUI试图通过不更新不可见的选项卡来提高效率。

    直接持有SwiftUI View 由于SwiftUI的声明性,不建议使用。由于操作SwiftUI的托管状态( @Published , @State )在的更新周期内 UIViewRepresentable 可能导致未定义的行为或错误,另一种方法是封装 WK Web视图 SwiftUI可以观察到的专用类中的管理逻辑。这避免了改变 @已发布 @州 更新周期内的变量。

    创建一个可以观察到的专用WebViewManager类:

    class WebViewManager: ObservableObject {
        var webView: WKWebView?
        var link: String
        
        init(link: String) {
            self.link = link
            createWebView()
        }
        
        func createWebView() {
            let webConfiguration = WKWebViewConfiguration()
            webConfiguration.allowsInlineMediaPlayback = true
            self.webView = WKWebView(frame: .zero, configuration: webConfiguration)
            // additional setup logic here
        }
        
        func destroyWebView() {
            self.webView?.loadHTMLString("", baseURL: nil)
            self.webView = nil
        }
    }
    

    修改 SmartReelView 单个视频视图 使用此 WebViewManager :

    struct SmartReelView: UIViewRepresentable {
        @ObservedObject var webViewManager: WebViewManager
    
        func makeUIView(context: Context) -> WKWebView {
            return webViewManager.webView ?? WKWebView()
        }
        
        func updateUIView(_ uiView: WKWebView, context: Context) {
            if webViewManager.webView == nil {
                webViewManager.createWebView()
            }
            // additional logic to load or reload the content
        }
    }
    
    struct SingleVideoView: View {
        let link: String
        @State private var isActive = true
        @ObservedObject var webViewManager: WebViewManager
    
        var body: some View {
            SmartReelView(webViewManager: webViewManager)
                .onAppear {
                    isActive = true
                    if webViewManager.webView == nil {
                        webViewManager.createWebView()
                    }
                }
                .onDisappear {
                    isActive = false
                    webViewManager.destroyWebView()
                }
        }
    }
    

    修改 ForEach 循环以创建 WebViewManager 每个 单个视频视图 :

    ForEach(arr, id: \.self) { id in
        SingleVideoView(link: id, webViewManager: WebViewManager(link: id))
        .tag(id)
    }
    

    这样 WebViewManager 处理的创建和销毁 WK Web视图 实例。这个 SmartReelView 单个视频视图 观察该管理器并做出相应的反应,而无需直接修改 @州 @已发布 更新周期内的变量。


    我仍然会考虑 WK Web视图 实例,包括维护一个可重用视图的集合,在需要时分发它们,并在不再使用时将它们返回到池中。

    一个简化的示例(重点关注WebView池)将包括第一个 WebViewPool 经理
    该管理器将处理池化的逻辑:

    class WebViewPool {
        private var pool: [WKWebView] = []
        
        func getWebView() -> WKWebView {
            if let webView = pool.first {
                pool.removeFirst()
                return webView
            } else {
                // Create a new web view, configure it as needed
                let webView = WKWebView()
                return webView
            }
        }
        
        func returnWebView(_ webView: WKWebView) {
            // Optionally clear the webView content
            webView.loadHTMLString("", baseURL: nil)
            
            pool.append(webView)
        }
    }
    

    然后,您可以在需要web视图的SwiftUI视图中创建此管理器的实例,例如 所有视频视图

    struct AllVideoView: View {
        @State private var webViewPool = WebViewPool()
        //... existing code
    }
    

    单个视频视图 SmartReelView ,您可以使用池在视图出现时获取web视图,并在视图消失时返回该视图。

    struct SingleVideoView: View {
        let link: String
        @Binding var webViewPool: WebViewPool
        //... existing code
        
        var body: some View {
            // existing code
            SmartReelView(link: link, webViewPool: $webViewPool)
                .onAppear {
                    // Check out a WebView when appearing
                }
                .onDisappear {
                    // Return WebView when disappearing
                }
        }
    }
    
    struct SmartReelView: UIViewRepresentable {
        let link: String
        @Binding var webViewPool: WebViewPool
        var webView: WKWebView?
        
        func makeUIView(context: Context) -> WKWebView {
            webView = webViewPool.getWebView()
            // existing code
            return webView!
        }
        
        func updateUIView(_ uiView: WKWebView, context: Context) {
            // existing code
        }
        
        func dismantleUIView(_ uiView: WKWebView, coordinator: Coordinator) {
            webViewPool.returnWebView(uiView)
        }
    }
    

    这并不涵盖所有边缘案例。管理web视图的生命周期(签出和返回)需要更加细致。根据您的需要,您不仅可以在视图出现时,还可以在需要加载新视频时签出网络视图。

    尽管如此,这个想法仍然存在:通过这种方式重用web视图,您可以最大限度地减少创建和销毁web视图实例的开销,这将提高应用程序的性能。