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 startChromiumLocked
est ensureChromiumStartedLocked
appelé 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, ensureChromiumStartedLocked
qui appelle la méthode ? Grâce à une recherche approfondie dans les documents, nous WebViewChromiumAwInit.java
pouvons 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.startChromiumLocked
est ensureChromiumStartedLocked
appelée par la méthode, et ensureChromiumStartedLocked
la 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 WebViewChromiumFactoryProvider
de son rôle et WebViewChromiumFactoryProvider
il implémente WebViewFactoryProvider
l'interface. Une compréhension simple est qu'il WebView
s'agit d'une usine. Si l'application veut être créée WebView
, cela se fera via WebViewFactoryProvider
la 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. WebViewChromiumFactoryProvider
Dans l'implémentation de la méthode d'interface, WebViewChromiumAwInit
une 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.java
et en implémentant les codes spécifiques de ces deux fichiers.WebViewChromiumAwInit.java
Pendant le processus d'initialisation,App appelle WebViewFactoryProvider
une méthode de la classe d'implémentation d'interface. Cette méthode appelle WebViewChromiumAwInit
une 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 WebViewFactoryProvider
certaine méthode d'interface de l'interface.
-getAwTracingController
-getAwProxyController
-startYourEngines
-getStatics
-getDefaultGeolocationPermissions
-getDefaultServiceWorkerController
-getWebIconDatabase
-getDefaultWebStorage
-getDefaultWebViewDatabase
Étant donné que WebView
le code de n'est pas intégré à l'application, WebView
le noyau utilisé par l'application est le code du noyau intégré et mis à niveau WebView
du 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 WebViewFactoryProvider
par réflexion .android.webkit.WebViewFactory
sProviderInstance
##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.getDefaultUserAgent
un Hook à la compilation et de rediriger vers defaultUserAgent
la 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 defaultUserAgent
change, 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.getDefaultUserAgent
obtenir 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.getDefaultUserAgent
la valeur et mettre à jour le cache.
Après un tel traitement, defaultUserAgent
l'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 defaultUserAgent
aussi 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
- Processus d'initialisation du démarrage
- Démarrez le processus Zygote au démarrage
- Démarrez le processus SystemServer au démarrage
- Pilote de reliure
- Processus de démarrage d'AMS
- Processus de démarrage du PMS
- Processus de démarrage du lanceur
- Android quatre composants principaux
- Service système Android - Processus de distribution des événements d'entrée
- Rendu sous-jacent Android - Analyse du code source du mécanisme de rafraîchissement de l'écran
- Analyse du code source Android en pratique