Cas d'optimisation du démarrage Android : dépannage d'initialisation inattendu de WebView

Lorsque je faisais de l'optimisation de startup à la fin de l'année dernière, j'avais un cas plutôt intéressant à partager avec vous. J'espère que vous pourrez comprendre de mon partage à quel point j'avais l'air bas, maladroit et efficace lorsque je faisais du dépannage et des réparations .

1. Phénomène

Lorsque nous avons utilisé Perfetto pour observer les performances du processus de démarrage de l'application, nous avons trouvé un temps d'initialisation inattendu de la vue Web de plusieurs dizaines de millisecondes à plusieurs centaines de millisecondes dans le thread de l'interface utilisateur (environnement machine : Xiaomi 10 pro), et ce code a été exécuté en ligne. machine de l'utilisateur. Cela peut prendre plus de temps.

Pourquoi est-il dit de manière inattendue :

  • Il n'y a aucune utilisation ni préchargement de WebView sur la page d'accueil
  • L'initialisation du noyau X5 se fait également après le processus de démarrage

2. Suivez les indices

Généralement, lorsque nous découvrons ce genre de problème, comment devons-nous y faire face ?

- Comprendre le processus . Si lors du dépannage des performances de démarrage, vous constatez que le thread principal (sous-) prend un temps qui ne répond pas aux attentes, la première étape consiste à découvrir comment ce code fastidieux est appelé.

- Découvrez où le code est appelé. Une fois que nous savons comment le code est appelé, nous pouvons trouver des moyens de le corriger. Si c'est parce que le code du projet est appelé au mauvais moment, retardez ou supprimez les appels concernés.

WebViewChromiumAwInit.java

Commençons ensuite par la première étape et comprenons le processus.Nous pouvons voir que la méthode système appelée par le bloc de code chronophage dans l'image est :

WebViewChromiumAwInit.startChromiumLocked, Étant donné que Perfetto ne peut pas voir les informations de pile liées à l'application, nous ne pouvons pas savoir directement quelle ligne de code l'a provoqué.

Suivez ensuite le code source de la vue Web pour voir la situation spécifique. Cliquez sur WebViewChromiumAwInit.java la page pour voir le code correspondant et constatez qu'il startChromiumLockedest ensureChromiumStartedLockedappelé par la méthode :

// This method is not private only because the downstream subclass needs to access it,
// it shouldn't be accessed from anywhere else.
/* package */ 
void ensureChromiumStartedLocked(boolean fromThreadSafeFunction) {
        assert Thread.holdsLock(mLock);
        if (mInitState == INIT_FINISHED) { // Early-out for the common case.
            return;
        }
        if (mInitState == INIT_NOT_STARTED) {
            // If we're the first thread to enter ensureChromiumStartedLocked, we need to determine
            // which thread will be the UI thread; declare init has started so that no other thread
            // will try to do this.
            mInitState = INIT_STARTED;
            setChromiumUiThreadLocked(fromThreadSafeFunction);
        }
        if (ThreadUtils.runningOnUiThread()) {
            // If we are currently running on the UI thread then we must do init now. If there was
            // already a task posted to the UI thread from another thread to do it, it will just
            // no-op when it runs.
            startChromiumLocked();
            return;
        }
        mIsPostedFromBackgroundThread = true;
        // If we're not running on the UI thread (because init was triggered by a thread-safe
        // function), post init to the UI thread, since init is *not* thread-safe.
        AwThreadUtils.postToUiThreadLooper(new Runnable() {
            @Override
            public void run() {
                synchronized (mLock) {
                    startChromiumLocked();
                }
            }
        });
        // Wait for the UI thread to finish init.
        while (mInitState != INIT_FINISHED) {
            try {
                mLock.wait();
            } catch (InterruptedException e) {
                // Keep trying; we can't abort init as WebView APIs do not declare that they throw
                // InterruptedException.
            }
        }
    }

Alors, ensureChromiumStartedLockedqui appelle la méthode ? Grâce à une recherche approfondie dans les documents, nous WebViewChromiumAwInit.javapouvons trouver les suspects suivants. Notre première réaction est “这也太多了吧,这咋排查啊”.

-getAwTracingController
-getAwProxyController
-startYourEngines
-getStatics
-getDefaultGeolocationPermissions
-getDefaultServiceWorkerController
-getWebIconDatabase
-getDefaultWebStorage
-getDefaultWebViewDatabase

public class WebViewChromiumAwInit {
    public AwTracingController getAwTracingController() {
        synchronized (mLock) {
            if (mAwTracingController == null) {
                ensureChromiumStartedLocked(true);
            }
        }
        return mAwTracingController;
    }
    public AwProxyController getAwProxyController() {
        synchronized (mLock) {
            if (mAwProxyController == null) {
                ensureChromiumStartedLocked(true);
            }
        }
        return mAwProxyController;
    }
    void startYourEngines(boolean fromThreadSafeFunction) {
        synchronized (mLock) {
            ensureChromiumStartedLocked(fromThreadSafeFunction);
        }
    }
    
    public SharedStatics getStatics() {
        synchronized (mLock) {
            if (mSharedStatics == null) {
                ensureChromiumStartedLocked(true);
            }
        }
        return mSharedStatics;
    }

    public GeolocationPermissions getDefaultGeolocationPermissions() {
        synchronized (mLock) {
            if (mDefaultGeolocationPermissions == null) {
                ensureChromiumStartedLocked(true);
            }
        }
        return mDefaultGeolocationPermissions;
    }

    public AwServiceWorkerController getDefaultServiceWorkerController() {
        synchronized (mLock) {
            if (mDefaultServiceWorkerController == null) {
                ensureChromiumStartedLocked(true);
            }
        }
        return mDefaultServiceWorkerController;
    }
    public android.webkit.WebIconDatabase getWebIconDatabase() {
        synchronized (mLock) {
            ensureChromiumStartedLocked(true);
            if (mWebIconDatabase == null) {
                mWebIconDatabase = new WebIconDatabaseAdapter();
            }
        }
        return mWebIconDatabase;
    }

    public WebStorage getDefaultWebStorage() {
        synchronized (mLock) {
            if (mDefaultWebStorage == null) {
                ensureChromiumStartedLocked(true);
            }
        }
        return mDefaultWebStorage;
    }

    public WebViewDatabase getDefaultWebViewDatabase(final Context context) {
        synchronized (mLock) {
            ensureChromiumStartedLocked(true);
            if (mDefaultWebViewDatabase == null) {
                mDefaultWebViewDatabase = new WebViewDatabaseAdapter(mFactory,
                        HttpAuthDatabase.newInstance(context, HTTP_AUTH_DATABASE_FILE),
                        mDefaultBrowserContext);
            }
        }
        return mDefaultWebViewDatabase;
    }
}

WebViewChromiumFactoryProvider.java

Après l'analyse simple ci-dessus, nous savons à peu près qu'elle WebViewChromiumAwInit.startChromiumLockedest ensureChromiumStartedLockedappelée par la méthode, et ensureChromiumStartedLockedla méthode sera appelée par les méthodes suivantes.Ensuite, notre prochain travail devra découvrir qui appelle les méthodes suivantes.

-getAwTracingController
-getAwProxyController
-startYourEngines
-getStatics
-getDefaultGeolocationPermissions
-getDefaultServiceWorkerController
-getWebIconDatabase
-getDefaultWebStorage
-getDefaultWebViewDatabase

Ici, j'aimerais partager une de mes méthodes natives. Nous voulons savoir où ces méthodes sont appelées. Ensuite, nous trouvons une méthode que nous ne reconnaissons pas et qui ne semble pas être mentionnée par les autres. Nous la recherchons sur Google. et on sélectionne la méthode d'un coup d'oeil getDefaultServiceWorkerController... Pas question, qui m'a dit que je ne te connaissais pas ? Bien que la méthode soit stupide, elle ne peut pas maintenir son efficacité. Alors nous l'avons déterré -WebViewChromiumFactoryProvider.java

Ayons une idée générale WebViewChromiumFactoryProviderde son rôle et WebViewChromiumFactoryProvideril implémente WebViewFactoryProviderl'interface. Une compréhension simple est qu'il WebViews'agit d'une usine. Si l'application veut être créée WebView, cela se fera via WebViewFactoryProviderla classe d'implémentation de l'interface createWebView, donc c'est en fait un modèle d'usine. Garantissez la compatibilité, la portabilité et l’évolutivité en faisant abstraction et en standardisant les API.

Nous avons également vu les appels à plusieurs méthodes listées ci-dessus dans ce fichier comme nous le souhaitions. WebViewChromiumFactoryProviderDans l'implémentation de la méthode d'interface, WebViewChromiumAwInitune série de méthodes sont appelées, comme suit :

//WebViewChromiumFactoryProvider.java
@Override
public WebViewProvider createWebView(WebView webView, WebView.PrivateAccess privateAccess) {
    return new WebViewChromium(this, webView, privateAccess, mShouldDisableThreadChecking);
}

//我们截取一段
    @Override
    public GeolocationPermissions getGeolocationPermissions() {
        return mAwInit.getDefaultGeolocationPermissions();
    }
    @Override
    public CookieManager getCookieManager() {
        return mAwInit.getDefaultCookieManager();
    }
    @Override
    public ServiceWorkerController getServiceWorkerController() {
        synchronized (mAwInit.getLock()) {
            if (mServiceWorkerController == null) {
                mServiceWorkerController = new ServiceWorkerControllerAdapter(
                        mAwInit.getDefaultServiceWorkerController());
            }
        }
        return mServiceWorkerController;
    }
    @Override
    public TokenBindingService getTokenBindingService() {
        return null;
    }
    @Override
    public android.webkit.WebIconDatabase getWebIconDatabase() {
        return mAwInit.getWebIconDatabase();
    }
    @Override
    public WebStorage getWebStorage() {
        return mAwInit.getDefaultWebStorage();
    }
    @Override
    public WebViewDatabase getWebViewDatabase(final Context context) {
        return mAwInit.getDefaultWebViewDatabase(context);
    }
    WebViewDelegate getWebViewDelegate() {
        return mWebViewDelegate;
    }
    WebViewContentsClientAdapter createWebViewContentsClientAdapter(WebView webView,
            Context context) {
        try (ScopedSysTraceEvent e = ScopedSysTraceEvent.scoped(
                     "WebViewChromiumFactoryProvider.insideCreateWebViewContentsClientAdapter")) {
            return new WebViewContentsClientAdapter(webView, context, mWebViewDelegate);
        }
    }
    void startYourEngines(boolean onMainThread) {
        try (ScopedSysTraceEvent e1 = ScopedSysTraceEvent.scoped(
                     "WebViewChromiumFactoryProvider.startYourEngines")) {
            mAwInit.startYourEngines(onMainThread);
        }
    }
    boolean hasStarted() {
        return mAwInit.hasStarted();
    }

3. Identifiez le problème

Nous avons une idée plus claire ci-dessus en lisant WebViewChromiumFactoryProvider.javaet en implémentant les codes spécifiques de ces deux fichiers.WebViewChromiumAwInit.java

Pendant le processus d'initialisation,App appelle WebViewFactoryProviderune méthode de la classe d'implémentation d'interface. Cette méthode appelle WebViewChromiumAwInitune ou plusieurs des méthodes suivantes. En fait, le problème devient clair : il suffit de savoir quelle ligne de code pendant la phase de démarrage de notre application appellera une WebViewFactoryProvidercertaine méthode d'interface de l'interface.

-getAwTracingController
-getAwProxyController
-startYourEngines
-getStatics
-getDefaultGeolocationPermissions
-getDefaultServiceWorkerController
-getWebIconDatabase
-getDefaultWebStorage
-getDefaultWebViewDatabase

Étant donné que WebViewle code de n'est pas intégré à l'application, WebViewle noyau utilisé par l'application est le code du noyau intégré et mis à niveau WebViewdu système Android, il n'est donc pas possible d'appeler des hooks via la transformation. Ici, nous utilisons la méthode de proxy dynamique pour Pour accrocher la méthode d'interface, nous générons un objet proxy via un proxy dynamique et remplaçons l'objet WebViewFactoryProviderpar réflexion .android.webkit.WebViewFactorysProviderInstance

    ##WebViewFactory
    @SystemApi
    public final class WebViewFactory{
        //...
        @UnsupportedAppUsage
        private static WebViewFactoryProvider sProviderInstance;
        //...
    }
    
    
    ##动态代理
    try {
        Class clas = Class.forName("android.webkit.WebViewFactory");
        Method method = clas.getDeclaredMethod("getProvider");
        method.setAccessible(true);
        Object obj = method.invoke(null);

        Object hookService = Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getSuperclass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Log.d("zttt", "hookService method: " + method.getName());
                        new RuntimeException(method.getName()).printStackTrace();
                        return method.invoke(obj, args);
                    }
                });

        Field field = clas.getDeclaredField("sProviderInstance");
        field.setAccessible(true);
        field.set(null, hookService);
    } catch (Exception e) {
        e.printStackTrace();
    }

Après avoir remplacé sProviderInstance, nous pouvons ajouter des points d'arrêt à notre logique de proxy pour le débogage, et enfin trouver l'initiateur de l'initialisation inattendue de WebView : WebSettings.getDefaultUserAgent.

4. Résolvez le problème

Le problème est résolu ici. Il vous suffit d'effectuer WebSettings.getDefaultUserAgentun Hook à la compilation et de rediriger vers defaultUserAgentla méthode appropriée avec le cache. S'il existe un cache local, il sera lu directement. S'il n'y a pas de cache local, il sera lu immédiatement . Grâce à mon travail précédent dans le projet Le cadre Hook configurable et facile à utiliser implémenté dans . Ce travail Hook à petite échelle peut être réalisé en moins d'une minute.

Bien sûr, il y a un autre problème qui doit être pris en compte ici, à savoir, lorsque la machine de l'utilisateur defaultUserAgentchange, comment le cache local peut-il être mis à jour à temps et le nouveau utilisé dans les requêtes réseau defaultUserAgent. Notre approche est la suivante :

  • Lorsqu'il n'y a pas de cache local, appelez immédiatement pour WebSettings.getDefaultUserAgentobtenir la valeur et mettre à jour le cache ;

  • Chaque fois que la phase de démarrage de l'application se termine, elle sera appelée dans le thread enfant pour obtenir WebSettings.getDefaultUserAgentla valeur et mettre à jour le cache.

Après un tel traitement, defaultUserAgentl'impact des modifications sera minimisé. La mise à niveau du système WebView elle-même est extrêmement rare. Dans ce cas, il est raisonnable pour nous d'abandonner l'exactitude des premières requêtes réseau lors de la prochaine ouverture de l'application. C'est defaultUserAgentaussi un cas classique où l'on considère le « rapport bénéfice-risque ».

5. Confirmez que le problème est résolu

Grâce au hook ci-dessus, nous reconditionnons et exécutons l'application, et l'heure pertinente n'est plus observée pendant la phase de démarrage.

Faites-le et arrêtez-le. Non seulement il est efficace de résoudre le problème, mais il est également efficace d'écrire un blog. Il est terminé en un certain temps. C'est comme un produit avant l'évaluation trimestrielle des performances. L'efficacité de formuler le plan et le lancer n’est qu’un mot, whoosh.

Afin d'aider chacun à comprendre l'optimisation des performances de manière plus complète et plus claire, nous avons préparé des notes de base pertinentes (y compris la logique sous-jacente) :https://qr18.cn/FVlo89

Notes de base sur l’optimisation des performances :https://qr18.cn/FVlo89

Optimisation du démarrage

, optimisation de la mémoire,

optimisation de l'interface utilisateur,

optimisation du réseau,

optimisation Bitmap et optimisation de la compression d'image : optimisation de la concurrence multithread et optimisation de l'efficacité de la transmission de données, optimisation des packages de volumeshttps://qr18.cn/FVlo89




« Cadre de surveillance des performances Android » :https://qr18.cn/FVlo89

"Manuel d'apprentissage du framework Android":https://qr18.cn/AQpN4J

  1. Processus d'initialisation du démarrage
  2. Démarrez le processus Zygote au démarrage
  3. Démarrez le processus SystemServer au démarrage
  4. Pilote de reliure
  5. Processus de démarrage d'AMS
  6. Processus de démarrage du PMS
  7. Processus de démarrage du lanceur
  8. Android quatre composants principaux
  9. Service système Android - Processus de distribution des événements d'entrée
  10. Rendu sous-jacent Android - Analyse du code source du mécanisme de rafraîchissement de l'écran
  11. Analyse du code source Android en pratique

おすすめ

転載: blog.csdn.net/maniuT/article/details/133176296