بهینه‌سازی راه‌اندازی WebView

وقتی برنامه شما برای اولین بار از WebView استفاده می‌کند، سیستم وظایف راه‌اندازی خاصی را انجام می‌دهد. این فرآیند راه‌اندازی سنگین است. به طور پیش‌فرض، این اتفاق به طور ضمنی در نخ رابط کاربری (UI thread) رخ می‌دهد، اولین باری که برنامه APIهای زیادی را در بسته‌های android.webkit یا androidx.webkit فراخوانی می‌کند، یا یک طرح‌بندی (layout) را که حاوی یک تگ WebView است، inflate می‌کند.

چرا این مهم است؟

از آنجا که این راه‌اندازی ضمنی کاملاً روی نخ اصلی اتفاق می‌افتد، برنامه شما را از پردازش ورودی کاربر مسدود می‌کند و خطر خطاهای عدم پاسخگویی برنامه (ANR) را به شدت افزایش می‌دهد. برای اطلاعات بیشتر در مورد نحوه مدیریت مدل اجرای تک‌رشته‌ای توسط اندروید، به مرور کلی فرآیندها و نخ‌ها مراجعه کنید.

محرک‌هایی برای راه‌اندازی ضمنی

راه‌اندازی ضمنی می‌تواند به روش‌های زیر فعال شود:

  • به صورت برنامه‌نویسی‌شده : فراخوانی APIهایی مانند WebSettings.getUserAgentString() .
  • استفاده از طرح‌بندی‌ها : فراخوانی setContentView() یا layoutInflater.inflate() روی یک منبع XML که شامل <WebView> است.

راه‌اندازی ضمنی همچنین می‌تواند بر معیارهای کسب‌وکار شما، مانند زمان راه‌اندازی برنامه و زمان اولین نمایش، تأثیر منفی بگذارد. اگر راه‌اندازی ضمنی برای برنامه شما بهینه نیست، به جای آن startUpWebView استفاده کنید.

این صفحه نحوه بهینه‌سازی عملکرد راه‌اندازی WebView با استفاده از startUpWebView API را مورد بحث قرار می‌دهد.

کنترل راه‌اندازی WebView را در دست بگیرید

برای بهبود عملکرد و به حداقل رساندن ANRها، از API startUpWebView موجود در کتابخانه Jetpack Webkit استفاده کنید. این API به شما امکان کنترل صریح بر زمان شروع به کار WebView را می‌دهد. این API بخش قابل توجهی از حجم کار راه‌اندازی را به یک thread پس‌زمینه منتقل می‌کند و هر کاری را که باید در thread UI انجام شود، به صورت تکه‌تکه انجام می‌دهد، نه یک بلوک بزرگ و یکپارچه. این کار thread UI شما را آزاد می‌کند تا سایر وظایف حیاتی برنامه را به صورت موازی انجام دهد و احتمال مسدود شدن تجربه کاربری را کاهش می‌دهد.

این API از تابع فراخوانی androidx.webkit.WebViewOutcomeReceiver استفاده می‌کند و به شما امکان می‌دهد مقداردهی‌های اولیه‌ی موفق را پیگیری کنید.

برای استفاده از این API، کتابخانه Jetpack Webkit را به فایل build.gradle خود اضافه کنید. مطمئن شوید که از نسخه ۱.۱۶.۰ یا بالاتر استفاده می‌کنید:

dependencies {
    implementation("androidx.webkit:webkit:1.16.0")
}

از API startUpWebView استفاده کنید

نحوه بهینه‌سازی جریان راه‌اندازی شما به این بستگی دارد که برنامه شما واقعاً چه زمانی نیاز به نمایش WebView دارد.

وقتی وب ویو در مسیر بحرانی قرار ندارد

اگر برنامه شما نیازی به بارگذاری فوری یک WebView ندارد، می‌توانید هزینه اولیه‌سازی را کاملاً پنهان کنید. startUpWebView را در اوایل چرخه حیات برنامه خود فراخوانی کنید و منتظر بمانید تا فراخوانی موفقیت‌آمیز اجرا شود.

در حالت ایده‌آل، باید قبل از فراخوانی سایر APIهای WebView، منتظر فراخوانی مجدد باشید. اگر startUpWebView را فعال کنید اما قبل از لمس سایر اجزای WebView منتظر اتمام آن نمانید، سیستم در حین انتظار برای تکمیل مقداردهی اولیه، نخ رابط کاربری را مسدود می‌کند. برنامه شما ممکن است از کار پس‌زمینه که قبلاً انجام شده است، تا حدودی از نظر عملکرد بهبود یابد، اما نه حداکثر مزیت.

وقتی WebView در مسیر بحرانی قرار دارد

اگر مسیر اصلی کاربر برنامه شما فوراً به WebView نیاز دارد، احتمالاً نمی‌توانید منتظر بمانید تا راه‌اندازی WebView کامل شود. در این سناریو، شما همچنان باید startUpWebView در اسرع وقت در چرخه حیات برنامه (مانند Application.onCreate ) فراخوانی کنید، اما منتظر شروع فراخوانی مجدد نمانید. در عوض، در صورت نیاز، مستقیماً از APIهای WebView استفاده کنید.

برای به دست آوردن حداکثر بهره از راه‌اندازی ناهمزمان، نمونه‌سازی یک WebView یا فراخوانی APIهای WebView را تا زمانی که هیچ عملیات نخ رابط کاربری مسیر بحرانی دیگری برای اجرا باقی نمانده باشد (مانند متورم کردن سلسله مراتب طرح‌بندی، مقداردهی اولیه SDKهای دیگر یا ترسیم فریم اولیه) به تعویق بیندازید.

اگر startUpWebView را فراخوانی کنید و بلافاصله پس از آن APIهای WebView را در نخ اصلی فراخوانی کنید، نخ رابط کاربری منتظر مقداردهی اولیه می‌ماند تا به آن برسد. در این سناریو، هیچ مزیت عملکردی وجود ندارد.

اگر استفاده از WebView می‌تواند در مسیر بحرانی قرار گیرد اما شما نمی‌خواهید WebView را به طور کامل راه‌اندازی کنید، می‌توانید به صورت انتخابی وظایف راه‌اندازی WebView را که قابلیت اجرا در یک thread پس‌زمینه را دارند، اجرا کنید و thread رابط کاربری را برای سایر وظایف حیاتی برنامه آزاد کنید. برای این کار، می‌توانید shouldRunUiThreadStartUpTasks(false) استفاده کنید.

بعداً در چرخه حیات برنامه‌تان، می‌توانید startUpWebView دوباره با shouldRunUiThreadStartUpTasks(true) فراخوانی کنید تا وظایف راه‌اندازی باقی‌مانده در نخ UI را به پایان برسانید. اینکه آیا در آن مرحله منتظر فراخوانی مجدد باشید یا خیر، بستگی به این دارد که آیا استفاده از WebView در مسیر بحرانی قرار دارد یا خیر.

مثال پیاده‌سازی

این API از تابع فراخوانی androidx.webkit.WebViewOutcomeReceiver استفاده می‌کند و به شما امکان می‌دهد مقداردهی‌های اولیه‌ی موفق را پیگیری کنید یا خطاهای تشخیصی را مدیریت کنید.

فراخوانی startUpWebView چندین بار از قسمت‌های مختلف برنامه، بی‌خطر است. توصیه می‌کنیم از پیاده‌سازی حلقه‌ی تلاش مجدد ساده خودداری کنید.

نمونه کد زیر نحوه استفاده از WebViewCompat.startUpWebView API را برای مقداردهی اولیه ناهمزمان نشان می‌دهد.

کاتلین

import android.content.Context
import android.util.Log
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewOutcomeReceiver
import androidx.webkit.WebViewStartUpConfig
import androidx.webkit.WebViewStartUpResult
import androidx.webkit.WebViewStartupException
import java.util.concurrent.Executors

fun initializeWebView(context: Context) {
    // 1. Create a startup configuration specifying the background thread
    // that WebView will use to run its initialization tasks.
    val startUpConfig = WebViewStartUpConfig.Builder(
        Executors.newSingleThreadExecutor()
    ).build()

    // 2. Trigger WebView startup asynchronously
    WebViewCompat.startUpWebView(
        context,
        startUpConfig,
        object : WebViewOutcomeReceiver<WebViewStartUpResult, WebViewStartupException> {

            override fun onResult(result: WebViewStartUpResult) {
                // Success: The WebView has finished its background initialization.
                // This callback is guaranteed to be invoked on the UI thread.
                setupWebView()
            }

            override fun onError(error: WebViewStartupException) {
                // Failure: The initialization encountered a startup exception.
                Log.e("WebViewStartup", "Failed to initialize WebView", error)
            }
        }
    )
}

جاوا

import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.webkit.WebViewCompat;
import androidx.webkit.WebViewOutcomeReceiver;
import androidx.webkit.WebViewStartUpConfig;
import androidx.webkit.WebViewStartUpResult;
import androidx.webkit.WebViewStartupException;
import java.util.concurrent.Executors;

public void initializeWebView(Context context) {
    // 1. Create the startup configuration specifying the background thread pool
    // to handle internal non-UI initialization processes.
    WebViewStartUpConfig startUpConfig = new WebViewStartUpConfig.Builder(
            Executors.newSingleThreadExecutor()
    ).build();

    // 2. Trigger WebView startup asynchronously
    WebViewCompat.startUpWebView(
            context,
            startUpConfig,
            new WebViewOutcomeReceiver<WebViewStartUpResult, WebViewStartupException>() {

                @Override
                public void onResult(@NonNull WebViewStartUpResult result) {
                    // Success: The WebView has finished its background initialization.
                    // This callback is invoked directly on the UI thread.
                    setupWebView();
                }

                @Override
                public void onError(@NonNull WebViewStartupException error) {
                    // Failure: Handled using the concrete WebViewStartupException
                    Log.e("WebViewStartup", "Failed to initialize WebView", error);
                }
            }
    );
}

اشکال‌زدایی مشکلات راه‌اندازی ناهمزمان

اگر startUpWebView مزایای عملکردی مورد انتظار را به همراه نداشته باشد، اغلب به این دلیل است که WebView قبل از اجرای فراخوانی شما، به طور ضمنی در جای دیگری از برنامه شما مقداردهی اولیه می‌شود. این می‌تواند به دلایل زیر باشد:

  • کتابخانه‌های شخص ثالث یا SDKها در اوایل چرخه حیات برنامه راه‌اندازی می‌شوند.

  • ContentProviders که به APK شما تزریق می‌شوند و APIهای WebView را در هنگام راه‌اندازی برنامه فعال می‌کنند.

  • تورم‌های طرح‌بندی یا فراخوانی‌های برنامه‌نویسی (مانند واکشی رشته‌های عامل کاربر) که به طور غیرمنتظره‌ای زودتر رخ می‌دهند.

برای کمک به شما در تشخیص محل و دلیل وقوع این مقداردهی‌های اولیه‌ی غیرمنتظره، شیء WebViewStartUpResult قابلیت‌های حسابرسی داخلی را ارائه می‌دهد:

  • getUiThreadBlockingStartUpLocations() : فهرستی از اشیاء StartUpLocation را برمی‌گرداند که نشان‌دهنده مکان‌هایی هستند که وظایف راه‌اندازی WebView، نخ اصلی رابط کاربری را مسدود کرده‌اند.

  • getNonUiThreadBlockingStartUpLocations() : سایت‌های فراخوانی خاصی را برمی‌گرداند که در آن‌ها وظایف راه‌اندازی، نخ‌های پس‌زمینه را مسدود کرده‌اند.

هر StartUpLocation شامل یک ردپای پشته است که می‌توانید آن را ثبت یا بررسی کنید تا کلاس و متد دقیقی که مقداردهی اولیه را آغاز کرده است، پیدا کنید.

مثال پیاده‌سازی

شما می‌توانید این مکان‌ها را در داخل تابع onResult خود بررسی کنید تا مسیر راه‌اندازی خود را بررسی کنید:

override fun onResult(result: WebViewStartUpResult) {
    // Check if WebView startup was blocked on the UI thread prior to or during initialization
    val uiBlockingLocations = result.getUiThreadBlockingStartUpLocations()
    if (!uiBlockingLocations.isNullOrEmpty()) {
        for (location in uiBlockingLocations) {
            // Log the stack trace of the call site that triggered the UI-blocking startup
            Log.w("WebViewDebug", "WebView startup blocked the UI thread here:", location.getStack())
        }
    } else {
        Log.i("WebViewDebug", "Excellent! No UI-blocking WebView startup detected.")
    }

    // Check where background initialization tasks were executed
    val backgroundLocations = result.getNonUiThreadBlockingStartUpLocations()
    backgroundLocations?.forEach { location ->
        Log.d("WebViewDebug", "WebView background startup occurred at: ${location.getStack()}")
    }

    setupWebView()
}

نحوه استفاده از این داده‌ها در طول ممیزی

هنگام بررسی راه‌اندازی WebView برنامه خود، از استراتژی‌های زیر برای تجزیه و تحلیل داده‌های تشخیصی و رفع گلوگاه‌های عملکرد استفاده کنید:

  • به دنبال ردپاهای پشته غیرمنتظره بگردید: اگر getUiThreadBlockingStartUpLocations() خالی نیست، به ردپاهای پشته چاپ شده نگاه کنید. اگر کلاس‌هایی متعلق به SDKهای شخص ثالث یا اجزای غیرمنتظره می‌بینید، یک گلوگاه مقداردهی اولیه ضمنی پیدا کرده‌اید.

  • تأیید ترتیب فراخوانی: اگر خروجی‌های لاگ شما نشان می‌دهند که یک مقداردهی اولیه ضمنی قبل از فراخوانی دستی startUpWebView رخ داده است، باید مقداردهی اولیه startUpWebView خود را در برنامه خود به قبل‌تر منتقل کنید یا SDK مربوطه را طوری پیکربندی کنید که وظایف وابسته به WebView را به تأخیر بیندازد.

مهاجرت از راه‌حل‌های قبلی

در گذشته، ممکن بود از راه‌حل‌های صریح برای مجبور کردن مقداردهی اولیه WebView روی یک نخ پس‌زمینه، مانند واکشی رشته عامل کاربر، استفاده کنید.

این راه‌حل‌ها، شیوه‌های پشتیبانی نشده محسوب می‌شوند و رفتار اساسی آنها می‌تواند در نسخه‌های بعدی تغییر کند. اگر برنامه شما برای راه‌اندازی یا مدیریت راه‌اندازی WebView به هرگونه راه‌حل صریح و مستند نشده‌ای متکی است، توصیه می‌کنیم به جای آن از API startUpWebView استفاده کنید. API startUpWebView روی تمام نسخه‌های اندروید و WebView که توسط کتابخانه Jetpack Webkit پشتیبانی می‌شوند، کار می‌کند.

استفاده از پیاده‌سازی Jetpack Webkit به تضمین رفتار سازگار در کل اکوسیستم اندروید کمک می‌کند. یکی از مزایای کلیدی این API، انعطاف‌پذیری آن است: در دستگاه‌های قدیمی که بهینه‌سازی‌های جدیدتر در دسترس نیستند، API برابری عملکرد را با راه‌حل‌های دستی حفظ می‌کند. این به شما امکان می‌دهد مزایای راه‌اندازی مدرن را در دستگاه‌های جدیدتر بدون متحمل شدن جریمه عملکرد در دستگاه‌های قدیمی‌تر، اتخاذ کنید.

اگر با مشکلی مواجه شدید یا در مورد API مربوط به startUpWebView بازخوردی دارید، در ردیاب مشکلات عمومی، یک اشکال (bug) ثبت کنید.