Перейти к основному содержимому

WebView

Разработчику Архитектору Инженеру

WebView

WebView — это компонент пользовательского интерфейса, предоставляющий возможность отображения веб-контента внутри нативного приложения. WebView представляет собой встраиваемый браузерный движок, работающий в рамках процесса приложения и использующий стандартные веб-технологии (HTML, CSS, JavaScript) для рендеринга интерфейса.

Ключевые характеристики WebView:

  • Встраиваемость — компонент добавляется в иерархию представлений нативного приложения как обычный UI-элемент
  • Изолированность — работает в собственном процессе или песочнице, отделённой от основного кода приложения
  • Двусторонняя коммуникация — поддерживает обмен данными между JavaScript и нативным кодом
  • Контроль поведения — позволяет перехватывать навигацию, модифицировать запросы, внедрять скрипты

Архитектура WebView

Компоненты системы

КомпонентНазначениеУровень
Рендеринг-движокПреобразование HTML/CSS в пиксели на экранеВнутренний
JavaScript-движокИсполнение скриптов (V8, JavaScriptCore)Внутренний
Сетевой стекВыполнение HTTP-запросов, работа с кэшемВнутренний
API-слойПубличные методы для управления компонентомВнешний
Мост (Bridge)Канал связи между JS и нативным кодомПограничный

Процессная модель

Современные реализации WebView используют многопроцессную архитектуру:

  1. Основной процесс приложения — содержит нативный код и объект WebView
  2. Процесс рендеринга — отвечает за парсинг DOM, вычисление стилей, отрисовку
  3. GPU-процесс — выполняет композитинг слоёв и аппаратное ускорение
  4. Сетевой процесс — централизованно обрабатывает все HTTP-запросы
  5. Процесс расширений — изолирует выполнение плагинов и service workers

Разделение на процессы обеспечивает стабильность: сбой в рендеринге страницы сохраняет работоспособность основного приложения.

Загрузка WebView…

Платформенные реализации

Android WebView

Android WebView основан на движке Chromium и обновляется через Google Play независимо от версии ОС. Начиная с Android 7.0 (Nougat) используется общая кодовая база с Chrome.

Основные классы и интерфейсы:

import android.webkit.WebView
import android.webkit.WebViewClient
import android.webkit.WebChromeClient
import android.webkit.WebSettings

class WebContainerActivity : AppCompatActivity() {
private lateinit var webView: WebView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web_container)

webView = findViewById(R.id.webview)
configureWebView()
webView.loadUrl("https://example.com")
}

private fun configureWebView() {
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
allowFileAccess = false
cacheMode = WebSettings.LOAD_DEFAULT
mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
}

webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
return handleUrl(request.url.toString())
}

override fun onPageFinished(view: WebView, url: String) {
injectNativeBridge()
}
}

webView.addJavascriptInterface(
NativeBridge(),
"AndroidBridge"
)
}
}

iOS WKWebView

Начиная с iOS 8, Apple предоставляет фреймворк WebKit с классом WKWebView, заменившим устаревший UIWebView. WKWebView работает в отдельном процессе (WebContent), что повышает стабильность и безопасность.

import WebKit

class WebContainerViewController: UIViewController {
private var webView: WKWebView!
private var configuration: WKWebViewConfiguration!

override func viewDidLoad() {
super.viewDidLoad()
setupWebView()
loadContent()
}

private func setupWebView() {
configuration = WKWebViewConfiguration()
configuration.preferences.javaScriptEnabled = true
configuration.websiteDataStore = WKWebsiteDataStore.default()

let userScript = WKUserScript(
source: bridgeScript(),
injectionTime: .atDocumentStart,
forMainFrameOnly: true
)
configuration.userContentController.addUserScript(userScript)
configuration.userContentController.add(self, name: "nativeBridge")

webView = WKWebView(frame: view.bounds, configuration: configuration)
webView.navigationDelegate = self
webView.uiDelegate = self
view.addSubview(webView)
}

private func loadContent() {
let url = URL(string: "https://example.com")!
let request = URLRequest(url: url)
webView.load(request)
}

private func bridgeScript() -> String {
return """
window.NativeBridge = {
call: function(method, params) {
window.webkit.messageHandlers.nativeBridge.postMessage({
method: method,
params: params
});
}
};
"""
}
}

extension WebContainerViewController: WKScriptMessageHandler {
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
guard let body = message.body as? [String: Any],
let method = body["method"] as? String else { return }
handleNativeCall(method: method, params: body["params"])
}
}

Desktop-реализации

На desktop-платформах существуют собственные встраиваемые движки:

ПлатформаКомпонентДвижок
WindowsWebView2Chromium (Edge)
macOSWKWebViewWebKit
LinuxWebKitGTKWebKit
Cross-platformQt WebEngineChromium
Cross-platformCEFChromium

WebView2 для Windows представляет собой современный API, построенный поверх Chromium и распространяемый через Evergreen Bootstrapper:

using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.WinForms;

public partial class MainForm : Form
{
private WebView2 webView;

public MainForm()
{
InitializeComponent();
InitializeWebView();
}

private async void InitializeWebView()
{
webView = new WebView2
{
Dock = DockStyle.Fill
};
Controls.Add(webView);

var environment = await CoreWebView2Environment.CreateAsync();
await webView.EnsureCoreWebView2Async(environment);

webView.CoreWebView2.WebMessageReceived += OnWebMessage;
webView.CoreWebView2.AddWebResourceRequestedFilter(
"https://api.example.com/*",
CoreWebView2WebResourceContext.All
);

webView.CoreWebView2.Navigate("https://example.com");
}

private void OnWebMessage(object sender, CoreWebView2WebMessageReceivedEventArgs e)
{
var json = e.WebMessageAsJson;
ProcessMessage(json);
}
}

Мост между JavaScript и нативным кодом

Архитектура JavaScript Bridge

JavaScript Bridge — это механизм двусторонней коммуникации, позволяющий веб-контенту вызывать нативные функции и наоборот. Стандартный подход использует асинхронный обмен сообщениями в формате JSON.

Протокол обмена сообщениями:

// Сторона JavaScript
class NativeBridge {
constructor() {
this.callbacks = new Map();
this.callId = 0;
}

call(method, params = {}) {
return new Promise((resolve, reject) => {
const id = ++this.callId;
this.callbacks.set(id, { resolve, reject });

const message = {
id: id,
method: method,
params: params,
timestamp: Date.now()
};

this.sendToNative(JSON.stringify(message));
});
}

sendToNative(message) {
// Платформенно-зависимая реализация
if (window.AndroidBridge) {
window.AndroidBridge.postMessage(message);
} else if (window.webkit?.messageHandlers?.nativeBridge) {
window.webkit.messageHandlers.nativeBridge.postMessage(message);
}
}

handleResponse(response) {
const { id, result, error } = JSON.parse(response);
const callback = this.callbacks.get(id);
if (callback) {
if (error) {
callback.reject(new Error(error));
} else {
callback.resolve(result);
}
this.callbacks.delete(id);
}
}
}

window.bridge = new NativeBridge();

Регистрация нативных обработчиков

На стороне Android используется аннотация @JavascriptInterface:

class NativeBridge {
@JavascriptInterface
fun postMessage(message: String) {
val json = JSONObject(message)
val id = json.getInt("id")
val method = json.getString("method")
val params = json.getJSONObject("params")

when (method) {
"getDeviceInfo" -> handleDeviceInfo(id)
"shareContent" -> handleShare(id, params)
"openCamera" -> handleCamera(id, params)
"getGeolocation" -> handleGeolocation(id)
}
}

private fun handleDeviceInfo(callId: Int) {
val info = JSONObject().apply {
put("platform", "android")
put("version", Build.VERSION.RELEASE)
put("model", Build.MODEL)
}
sendResponse(callId, info)
}

private fun sendResponse(callId: Int, result: JSONObject) {
val response = JSONObject().apply {
put("id", callId)
put("result", result)
}
val js = "window.bridge.handleResponse('${response}');"
webView.post { webView.evaluateJavascript(js, null) }
}
}

Работа с локальным контентом

Загрузка из файловой системы

WebView поддерживает загрузку HTML-файлов, хранящихся в ресурсах приложения. Этот подход применяется для гибридных приложений, где интерфейс полностью размещён локально.

Android — размещение файлов в assets/:

webView.loadUrl("file:///android_asset/www/index.html")

iOS — использование loadFileURL с разрешением доступа:

let url = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "www")!
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())

Виртуальный хостинг

Виртуальный хостинг позволяет обслуживать локальные ресурсы через HTTPS-URL, что устраняет ограничения CORS и смешанного контента:

webView.setWebViewAssetLoader(
WebViewAssetLoader.Builder()
.setDomain("app.example.com")
.addPathHandler(
"/assets/",
WebViewAssetLoader.AssetsPathHandler(context)
)
.addPathHandler(
"/internal/",
WebViewAssetLoader.InternalStoragePathHandler(
context,
File(context.filesDir, "www")
)
)
.build()
)

webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
return assetLoader.shouldInterceptRequest(request.url)
}
}

webView.loadUrl("https://app.example.com/assets/index.html")

Безопасность WebView

Угрозы и меры защиты

УгрозаМеханизм защиты
Внедрение вредоносного JSContent Security Policy, валидация входных данных
Перехват навигацииПереопределение shouldOverrideUrlLoading, whitelist URL
Утечка данных через file://Отключение allowFileAccess, allowFileAccessFromFileURLs
Доступ к JavaScript-интерфейсамОграничение @JavascriptInterface необходимыми методами
MITM-атакиCertificate pinning, проверка HTTPS
XSS через пользовательский контентЭкранирование, sanitization

Настройка безопасной конфигурации

webView.settings.apply {
// Отключение доступа к файловой системе
allowFileAccess = false
allowContentAccess = false
allowFileAccessFromFileURLs = false
allowUniversalAccessFromFileURLs = false

// Ограничение JavaScript
javaScriptEnabled = true
javaScriptCanOpenWindowsAutomatically = false

// Смешанный контент запрещён
mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW

// Отключение отладки в production
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)

// Безопасное отображение
safeBrowsingEnabled = true
}

Certificate Pinning

Certificate Pinning обеспечивает проверку сертификата сервера по заранее известному отпечатку:

class PinnedWebViewClient : WebViewClient() {
private val expectedHash = "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="

override fun onReceivedSslError(
view: WebView,
handler: SslErrorHandler,
error: SslError
) {
val certificate = error.certificate
val publicKey = certificate.publicKey.encoded
val hash = Base64.encodeToString(
MessageDigest.getInstance("SHA-256").digest(publicKey),
Base64.NO_WRAP
)

if (hash == expectedHash) {
handler.proceed()
} else {
handler.cancel()
reportSecurityEvent(error)
}
}
}

Производительность и оптимизация

Стратегии ускорения загрузки

  1. Предзагрузка WebView — создание экземпляра в фоновом режиме до момента отображения
  2. Preconnect и Prefetch — заблаговременное установление соединений с необходимыми доменами
  3. Service Workers — кэширование ресурсов и офлайн-работа
  4. Code splitting — разделение JavaScript на модули с ленивой загрузкой
  5. Сжатие ресурсов — Brotli/Gzip, минификация, оптимизация изображений

Предзагрузка на Android

class WebViewPreloader(private val context: Context) {
private var cachedWebView: WebView? = null

fun preload() {
CoroutineScope(Dispatchers.Main).launch {
cachedWebView = WebView(context.applicationContext).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
}
cachedWebView?.loadUrl("about:blank")
}
}

fun obtain(): WebView {
return cachedWebView ?: WebView(context)
}

fun destroy() {
cachedWebView?.destroy()
cachedWebView = null
}
}

Мониторинг производительности

WebKit и Chromium предоставляют API для сбора метрик:

// Performance Observer для отслеживания ключевых метрик
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
const metric = {
name: entry.name,
type: entry.entryType,
duration: entry.duration,
startTime: entry.startTime
};
window.bridge.call('reportMetric', metric);
});
});

observer.observe({
entryTypes: ['navigation', 'resource', 'paint', 'largest-contentful-paint']
});

// Измерение Core Web Vitals
const metrics = {
fcp: 0, // First Contentful Paint
lcp: 0, // Largest Contentful Paint
cls: 0, // Cumulative Layout Shift
fid: 0, // First Input Delay
inp: 0 // Interaction to Next Paint
};

new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'largest-contentful-paint') {
metrics.lcp = entry.renderTime || entry.loadTime;
}
}
}).observe({ type: 'largest-contentful-paint', buffered: true });

Отладка WebView

Удалённая отладка

Удалённая отладка WebView выполняется через Chrome DevTools для Android и Safari Web Inspector для iOS.

Включение отладки на Android:

if (BuildConfig.DEBUG) {
WebView.setWebContentsDebuggingEnabled(true)
}

После включения устройство подключается к компьютеру, и в Chrome по адресу chrome://inspect отображается список доступных WebView-инстансов.

Инструменты DevTools включают:

  • Elements — инспекция DOM и CSS
  • Console — выполнение JavaScript и просмотр логов
  • Network — анализ HTTP-трафика
  • Performance — профилирование рендеринга
  • Memory — поиск утечек памяти
  • Application — работа с хранилищами и Service Workers

Логирование из WebView

Перенаправление консольных сообщений JavaScript в нативные логи:

webView.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(message: ConsoleMessage): Boolean {
val tag = "WebView[${message.sourceId()}:${message.lineNumber()}]"
when (message.messageLevel()) {
ConsoleMessage.MessageLevel.ERROR -> Log.e(tag, message.message())
ConsoleMessage.MessageLevel.WARNING -> Log.w(tag, message.message())
ConsoleMessage.MessageLevel.DEBUG -> Log.d(tag, message.message())
else -> Log.i(tag, message.message())
}
return true
}
}

Гибридные фреймворки на основе WebView

Сравнительная таблица

ФреймворкПлатформыЯзык разработкиДвижок
Apache CordovaiOS, Android, WindowsJavaScriptПлатформенный WebView
Ionic CapacitoriOS, Android, WebJavaScript/TypeScriptПлатформенный WebView
React Native (WebView)iOS, AndroidJavaScript/TypeScriptПлатформенный WebView
Flutter WebViewiOS, Android, WebDartПлатформенный WebView
TauriDesktop, MobileRust + JavaScriptПлатформенный WebView
ElectronDesktopJavaScriptВстроенный Chromium

Архитектура Capacitor

Capacitor предоставляет унифицированный API поверх нативных WebView:

import { Camera, CameraResultType } from '@capacitor/camera';
import { Geolocation } from '@capacitor/geolocation';
import { Filesystem, Directory } from '@capacitor/filesystem';

async function capturePhoto() {
const photo = await Camera.getPhoto({
quality: 90,
allowEditing: false,
resultType: CameraResultType.Uri
});

const coordinates = await Geolocation.getCurrentPosition();

const fileName = `photo_${Date.now()}.jpg`;
await Filesystem.writeFile({
path: fileName,
data: photo.base64String,
directory: Directory.Documents
});

return {
photo: photo.webPath,
location: {
latitude: coordinates.coords.latitude,
longitude: coordinates.coords.longitude
}
};
}

Сравнение подходов к разработке

WebView против нативного UI

КритерийWebViewНативный UI
Скорость разработкиВысокаяСредняя
ПроизводительностьОграничена движкомМаксимальная
Доступ к API устройстваЧерез мостПрямой
Размер приложенияКомпактныйЗависит от сложности
Обновления интерфейсаБез переиздания приложенияТребуют выпуска новой версии
Офлайн-работаТребует Service WorkersВстроенная
Анимации и жестыОграничены CSS/JSПолный контроль

Критерии выбора WebView

WebView подходит для следующих сценариев:

  • Контентные приложения — новостные агрегаторы, блоги, документация
  • Гибридные приложения — сочетание нативного каркаса и веб-контента
  • Прототипирование — быстрая проверка гипотез с последующей нативной реализацией
  • Кроссплатформенные решения — единая кодовая база для iOS и Android
  • Внутренние инструменты — корпоративные приложения с частыми изменениями UI
  • Отображение внешнего контента — интеграция сторонних сервисов

Жизненный цикл и управление памятью

Правильное освобождение ресурсов

WebView занимает значительный объём памяти и требует явного уничтожения:

override fun onDestroy() {
webViewContainer?.removeView(webView)
webView?.apply {
stopLoading()
webChromeClient = null
webViewClient = WebViewClient()
clearHistory()
removeAllViews()
destroy()
}
webView = null
super.onDestroy()
}

Обработка конфигурационных изменений

Для предотвращения пересоздания WebView при повороте экрана:

<activity
android:name=".WebContainerActivity"
android:configChanges="orientation|screenSize|keyboardHidden" />

Расширенные возможности

Кастомные URL-схемы

Регистрация собственных схем для обработки ссылок внутри приложения:

class CustomSchemeHandler : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
val uri = request.url
return when (uri.scheme) {
"myapp" -> handleCustomScheme(uri)
"tel" -> openDialer(uri)
"mailto" -> openEmailClient(uri)
"intent" -> resolveAndroidIntent(uri)
else -> false
}
}

private fun handleCustomScheme(uri: Uri): Boolean {
when (uri.host) {
"profile" -> navigateToProfile(uri.getQueryParameter("id"))
"product" -> navigateToProduct(uri.getQueryParameter("sku"))
"checkout" -> startCheckoutFlow()
}
return true
}
}

Инъекция пользовательских стилей

Внедрение CSS после загрузки страницы для адаптации внешнего вида:

func applyCustomStyles() {
let css = """
body { background-color: #1a1a1a !important; color: #ffffff !important; }
.header { display: none !important; }
.content { padding: 16px !important; }
a { color: #4a9eff !important; }
"""
let js = """
var style = document.createElement('style');
style.textContent = `\(css)`;
document.head.appendChild(style);
"""
webView.evaluateJavaScript(js, completionHandler: nil)
}

Печать и экспорт в PDF

Сохранение содержимого WebView в виде PDF-документа:

fun exportToPdf(webView: WebView, outputFile: File) {
val adapter = webView.createPrintDocumentAdapter("document")
val printManager = context.getSystemService(Context.PRINT_SERVICE) as PrintManager
val attributes = PrintAttributes.Builder()
.setMediaSize(PrintAttributes.MediaSize.ISO_A4)
.setResolution(PrintAttributes.Resolution("pdf", "pdf", 300, 300))
.setMinMargins(PrintAttributes.Margins.NO_MARGINS)
.build()

adapter.onLayout(null, attributes, null, object : PrintDocumentAdapter.LayoutResultCallback() {
override fun onLayoutFinished(info: PrintDocumentInfo, changed: Boolean) {
adapter.onWrite(
arrayOf(PageRange.ALL_PAGES),
ParcelFileDescriptor.open(outputFile, ParcelFileDescriptor.MODE_CREATE or ParcelFileDescriptor.MODE_WRITE_ONLY),
attributes,
object : PrintDocumentAdapter.WriteResultCallback() {
override fun onWriteFinished(pages: Array<out PageRange>) {
Log.i("PDF", "Export completed: ${outputFile.absolutePath}")
}
}
)
}
}, null)
}

См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).

Освоение главы0%