Android Method for Solving WebView Multi process Crash

  • 2021-12-11 19:13:50
  • OfStack

Directory problem Problem analysis Solutions

Problem

On the android 9.0 system, if multiple processes use WebView, it is necessary to use the officially provided api to set suffix for the data folder of webview in the child process:


WebView.setDataDirectorySuffix(suffix);

Otherwise, the following error will be reported:


Using WebView from more than one process at once with the same data directory is not supported. https://crbug.com/558377

1 com.android.webview.chromium.WebViewChromiumAwInit.startChromiumLocked(WebViewChromiumAwInit.java:63)
2 com.android.webview.chromium.WebViewChromiumAwInitForP.startChromiumLocked(WebViewChromiumAwInitForP.java:3)
3 com.android.webview.chromium.WebViewChromiumAwInit$3.run(WebViewChromiumAwInit.java:3)
4 android.os.Handler.handleCallback(Handler.java:873)
5 android.os.Handler.dispatchMessage(Handler.java:99)
6 android.os.Looper.loop(Looper.java:220)
7 android.app.ActivityThread.main(ActivityThread.java:7437)
8 java.lang.reflect.Method.invoke(Native Method)
9 com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:500)
10 com.android.internal.os.ZygoteInit.main(ZygoteInit.java:865)

After using the official method, the problem is only reduced by one part, and a large number of crash messages of this problem can still be received from the background of bugly, so that they all rush to the crash problem Top3.

Problem analysis

From the source code analysis, the call chain finally calls the lock method in the AwDataDirLock class.


public class WebViewChromiumAwInit {
 protected void startChromiumLocked() {
   ...
   AwBrowserProcess.start();
   ... 
 }
}
public final class AwBrowserProcess {
 public static void start() {
   ...
   AwDataDirLock.lock(appContext);
}

AwDataDirLock.java


abstract class AwDataDirLock {
 private static final String TAG = "AwDataDirLock";
 private static final String EXCLUSIVE_LOCK_FILE = "webview_data.lock";
 // This results in a maximum wait time of 1.5s
 private static final int LOCK_RETRIES = 16;
 private static final int LOCK_SLEEP_MS = 100;
 private static RandomAccessFile sLockFile;
 private static FileLock sExclusiveFileLock;

 static void lock(final Context appContext) {
  try (ScopedSysTraceEvent e1 = ScopedSysTraceEvent.scoped("AwDataDirLock.lock");
    StrictModeContext ignored = StrictModeContext.allowDiskWrites()) {
   if (sExclusiveFileLock != null) {
    // We have already called lock() and successfully acquired the lock in this process.
    // This shouldn't happen, but is likely to be the result of an app catching an
    // exception thrown during initialization and discarding it, causing us to later
    // attempt to initialize WebView again. There's no real advantage to failing the
    // locking code when this happens; we may as well count this as the lock being
    // acquired and let init continue (though the app may experience other problems
    // later).
    return;
   }
   // If we already called lock() but didn't succeed in getting the lock, it's possible the
   // app caught the exception and tried again later. As above, there's no real advantage
   // to failing here, so only open the lock file if we didn't already open it before.
   if (sLockFile == null) {
    String dataPath = PathUtils.getDataDirectory();
    File lockFile = new File(dataPath, EXCLUSIVE_LOCK_FILE);
    try {
   // Note that the file is kept open intentionally.
     sLockFile = new RandomAccessFile(lockFile, "rw");
    } catch (IOException e) {
    // Failing to create the lock file is always fatal; even if multiple processes
    // are using the same data directory we should always be able to access the file
    // itself.
     throw new RuntimeException("Failed to create lock file " + lockFile, e);
    }
   }
   // Android versions before 11 have edge cases where a new instance of an app process can
   // be started while an existing one is still in the process of being killed. This can
   // still happen on Android 11+ because the platform has a timeout for waiting, but it's
   // much less likely. Retry the lock a few times to give the old process time to fully go
   // away.
   for (int attempts = 1; attempts <= LOCK_RETRIES; ++attempts) {
    try {
     sExclusiveFileLock = sLockFile.getChannel().tryLock();
    } catch (IOException e) {
    // Older versions of Android incorrectly throw IOException when the flock()
    // call fails with EAGAIN, instead of returning null. Just ignore it.
    }
    if (sExclusiveFileLock != null) {
     // We got the lock; write out info for debugging.
     writeCurrentProcessInfo(sLockFile);
     return;
    }
    // If we're not out of retries, sleep and try again.
    if (attempts == LOCK_RETRIES) break;
    try {
     Thread.sleep(LOCK_SLEEP_MS);
    } catch (InterruptedException e) {
    }
   }
   // We failed to get the lock even after retrying.
   // Many existing apps rely on this even though it's known to be unsafe.
   // Make it fatal when on P for apps that target P or higher
   String error = getLockFailureReason(sLockFile);
   boolean dieOnFailure = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
     && appContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P;
   if (dieOnFailure) {
    throw new RuntimeException(error);
   } else {
    Log.w(TAG, error);
   }
  }
 }

 private static void writeCurrentProcessInfo(final RandomAccessFile file) {
  try {
   // Truncate the file first to get rid of old data.
   file.setLength(0);
   file.writeInt(Process.myPid());
   file.writeUTF(ContextUtils.getProcessName());
  } catch (IOException e) {
   // Don't crash just because something failed here, as it's only for debugging.
   Log.w(TAG, "Failed to write info to lock file", e);
  }
 }

 private static String getLockFailureReason(final RandomAccessFile file) {
  final StringBuilder error = new StringBuilder("Using WebView from more than one process at "
    + "once with the same data directory is not supported. https://crbug.com/558377 "
    + ": Current process ");
  error.append(ContextUtils.getProcessName());
  error.append(" (pid ").append(Process.myPid()).append("), lock owner ");
  try {
   int pid = file.readInt();
   String processName = file.readUTF();
   error.append(processName).append(" (pid ").append(pid).append(")");
   // Check the status of the pid holding the lock by sending it a null signal.
   // This doesn't actually send a signal, just runs the kernel access checks.
   try {
    Os.kill(pid, 0);
    // No exception means the process exists and has the same uid as us, so is
    // probably an instance of the same app. Leave the message alone.
   } catch (ErrnoException e) {
    if (e.errno == OsConstants.ESRCH) {
     // pid did not exist - the lock should have been released by the kernel,
     // so this process info is probably wrong.
     error.append(" doesn't exist!");
    } else if (e.errno == OsConstants.EPERM) {
     // pid existed but didn't have the same uid as us.
     // Most likely the pid has just been recycled for a new process
     error.append(" pid has been reused!");
    } else {
     // EINVAL is the only other documented return value for kill(2) and should never
     // happen for signal 0, so just complain generally.
     error.append(" status unknown!");
    }
   }
  } catch (IOException e) {
   // We'll get IOException if we failed to read the pid and process name; e.g. if the
   // lockfile is from an old version of WebView or an IO error occurred somewhere.
   error.append(" unknown");
  }
  return error.toString();
 }
}

The lock method will try to lock the webview_data. lock file in the webview data directory 16 times in the for loop. The annotation also explains the reason for this: the possible extreme situation is that a new process starts when an old process is being killed. It seems that Google engineers also have a headache about this problem; If the lock succeeds, the process id and the process name are written to the file, and if the lock fails, an exception is thrown. Therefore, the principle of detecting whether there are multiple processes sharing WebView data directory above android9.0 is that the process holds the lock of webview_data. lock file in WebView data directory. Therefore, if a child process tries to lock the same file, it will cause the application to crash.

Solutions

At present, most mobile phones will automatically restart the application when the application crashes. Guess that when the mobile phone system runs slowly, a new process will start when an old process is being killed. Since a failure to acquire a file lock will cause a crash, And this file is only used for locking to determine whether there is a multi-process shared WebView data directory, Every time the lock is successful, the corresponding process information will be rewritten, so we can try to lock the file when the application starts, delete the file and recreate it if the lock fails, and release the lock immediately if the lock is successful, so that when the system tries to lock, the lock can be successfully locked in theory, thus avoiding the occurrence of this problem.


private static void handleWebviewDir(Context context) {
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
   return;
  }
  try {
   String suffix = "";
   String processName = getProcessName(context);
   if (!TextUtils.equals(context.getPackageName(), processName)) {// Judgment is not equal to the default process name 
    suffix = TextUtils.isEmpty(processName) ? context.getPackageName() : processName;
    WebView.setDataDirectorySuffix(suffix);
    suffix = "_" + suffix;
   }
   tryLockOrRecreateFile(context,suffix);
  } catch (Exception e) {
   e.printStackTrace();
  }
 }

 @TargetApi(Build.VERSION_CODES.P)
 private static void tryLockOrRecreateFile(Context context,String suffix) {
  String sb = context.getDataDir().getAbsolutePath() +
    "/app_webview"+suffix+"/webview_data.lock";
  File file = new File(sb);
  if (file.exists()) {
   try {
    FileLock tryLock = new RandomAccessFile(file, "rw").getChannel().tryLock();
    if (tryLock != null) {
     tryLock.close();
    } else {
     createFile(file, file.delete());
    }
   } catch (Exception e) {
    e.printStackTrace();
    boolean deleted = false;
    if (file.exists()) {
     deleted = file.delete();
    }
    createFile(file, deleted);
   }
  }
 }

 private static void createFile(File file, boolean deleted){
  try {
   if (deleted && !file.exists()) {
    file.createNewFile();
   }
  } catch (Exception e) {
   e.printStackTrace();
  }
 }

After using this scheme, the number of crashes of this problem is reduced by more than 90%. Perhaps Google engineers should consider another technical solution to detect whether there are multiple processes sharing WebView data directories in the application.

The above is Android to solve WebView multi-process crash method details, more about Android to solve WebView multi-process crash information please pay attention to other related articles on this site!


Related articles: