Swift中WebView实例过多致设备过热的解决方案咨询
解决方案:控制WKWebView实例销毁以优化资源占用
问题背景
开发仿TikTok应用时,采用垂直TabView展示YouTube Shorts视频,每个视频对应一个WKWebView实例。滚动时旧WebView实例不会自动销毁,导致资源占用过高、设备发热。需求是:当用户当前位置与目标视频的距离超过2个位置时,自动销毁对应WebView,同时保留最近的几个实例以支持回滚查看。
实现思路
- 用ViewModel统一管理视频列表与当前选中状态,避免分散的状态逻辑
- 给每个视频视图添加销毁标记,通过索引差判断是否触发销毁
- 在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
相关产品推荐
相关产品推荐

