You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift中WebView实例过多致设备过热的解决方案咨询

解决方案:控制WKWebView实例销毁以优化资源占用

问题背景

开发仿TikTok应用时,采用垂直TabView展示YouTube Shorts视频,每个视频对应一个WKWebView实例。滚动时旧WebView实例不会自动销毁,导致资源占用过高、设备发热。需求是:当用户当前位置与目标视频的距离超过2个位置时,自动销毁对应WebView,同时保留最近的几个实例以支持回滚查看。

实现思路

  1. 用ViewModel统一管理视频列表与当前选中状态,避免分散的状态逻辑
  2. 给每个视频视图添加销毁标记,通过索引差判断是否触发销毁
  3. 在WebView封装视图中实现完整的销毁与重建流程,确保资源彻底释放

完整代码实现

核心ViewModel

首先定义VideoModel作为环境对象,管理视频数据和选中状态:

import SwiftUI

class VideoModel: ObservableObject {
    @Published var selected: String = ""
    @Published var VideosToShow: [Video] = []
    
    struct Video: Identifiable {
        let id = UUID()
        let videoID: String
    }
    
    init() {
        let videoIDs = ["-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"]
        VideosToShow = videoIDs.map { Video(videoID: $0) }
        selected = VideosToShow.first?.videoID ?? ""
    }
}

单视频视图(SingleVideoView)

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) { _ in
            guard viewModel.selected != link else { return }
            
            isVideoPlaying = false
            
            guard let currentIndex = viewModel.VideosToShow.firstIndex(where: { $0.videoID == viewModel.selected }),
                  let videoIndex = viewModel.VideosToShow.firstIndex(where: { $0.videoID == link }) else {
                return
            }
            
            // 当前视频在目标视频之后且超过2个位置时,触发销毁
            if currentIndex - videoIndex > 2 && !destroy {
                destroy = true
                print("销毁视频索引: \(videoIndex)")
            }
        }
    }
}

WebView封装视图(SmartReelView)

struct SmartReelView: UIViewRepresentable {
    let link: String
    @Binding var isPlaying: Bool
    @Binding var destroy: Bool
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIView(context: Context) -> WKWebView {
        createWebView(context: context)
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        // 执行销毁逻辑
        if destroy && uiView.navigationDelegate != nil {
            destroyWebView(uiView)
        }
        
        // 更新播放状态
        let jsString = """
                isPlaying = \(isPlaying ? "true" : "false");
                watchPlayingState();
            """
        uiView.evaluateJavaScript(jsString, completionHandler: nil)
    }
    
    // 复用WebView创建逻辑
    private func createWebView(context: Context) -> WKWebView {
        let webConfiguration = WKWebViewConfiguration()
        webConfiguration.allowsInlineMediaPlayback = true
        let webView = WKWebView(frame: .zero, configuration: webConfiguration)
        webView.navigationDelegate = context.coordinator
        webView.scrollView.isScrollEnabled = false
        loadInitialContent(in: webView)
        return webView
    }
    
    // 彻底销毁WebView释放资源
    private func destroyWebView(_ webView: WKWebView) {
        print("已销毁WebView")
        webView.navigationDelegate = nil
        webView.stopLoading()
        webView.removeFromSuperview()
        
        // 重置标记,支持后续回滚重建
        DispatchQueue.main.async {
            self.destroy = false
        }
    }
    
    class Coordinator: NSObject, WKNavigationDelegate {
        var parent: SmartReelView

        init(_ parent: SmartReelView) {
            self.parent = parent
        }
        
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            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.loadHTMLString(embedHTML, baseURL: nil)
    }
}

主视图(AllVideoView)

struct AllVideoView: View {
    @StateObject private var viewModel = VideoModel()

    var body: some View {
        ZStack {
            Color.black.edgesIgnoringSafeArea([.bottom, .top])
            TabView(selection: $viewModel.selected){
                ForEach(viewModel.VideosToShow) { video in
                    SingleVideoView(link: video.videoID)
                        .tag(video.videoID)
                }
                .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))
        }
        .environmentObject(viewModel)
    }
}

func widthOrHeight(width: Bool) -> CGFloat {
    let scenes = UIApplication.shared.connectedScenes
    let windowScene = scenes.first as? UIWindowScene
    let window = windowScene?.windows.first
    
    return width ? (window?.screen.bounds.width ?? 0) : (window?.screen.bounds.height ?? 0)
}

关键优化点

  • 精准销毁时机:通过索引差判断,只销毁超出2个位置的WebView,保留最近实例支持回滚
  • 彻底资源释放:停止加载、移除代理、从父视图移除WebView,避免内存泄漏
  • 状态自动重置:销毁后重置标记,用户回滚时自动重建WebView
  • 统一状态管理:用ViewModel集中管理视频数据和选中状态,逻辑更清晰

内容的提问来源于stack exchange,提问作者Ahmed Zaidan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 20:57:04