过多的 Web 视图实例导致手机过热 Swift

问题描述 投票:0回答:1

您好,我正在使用 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
    }
}
html swift swiftui webview wkwebview
1个回答
0
投票

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

但是,由于您专门询问当用户超过 2 个视频时如何销毁 Web 视图,因此您可以实现逻辑来手动取消分配这些 Web 视图并清除其内容。

要手动销毁

WKWebView
,您需要:

  • 从其超级视图中删除 Web 视图(如果有)。
  • 将其
    navigationDelegate
    UIDelegate
    设置为
    nil
  • 调用其上的
    stopLoading
    方法。
  • 将 Web 视图本身设置为
    nil
    (如果没有对 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

将此索引传递给每个

SingleVideoView

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

最后,只要

activeIndex
的选择发生变化,就更新
TabView

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


我仍然会考虑使用

WKWebView
实例池,这涉及维护可重用视图的集合,在需要时分发它们,并在不再使用时将它们返回到池中。

一个简化的示例(重点关注 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 视图中创建此管理器的实例,例如在

AllVideoView
中。

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

并且在

SingleVideoView
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 视图实例的开销,这应该会提高应用程序的性能。

© www.soinside.com 2019 - 2024. All rights reserved.