問題:
java.lang.RuntimeException: Using WebView from more than one process at once with the same data directory is not supported
Fatal Exception: java.lang.RuntimeException: Using WebView from more than one process at once with the same data directory is not supported.
https://crbug.com/558377 : Current process com.xx.xxapp(pid 13862), lock owner com.xx.xx.xxAPP (pid 13559)
at org.chromium.android_webview.AwDataDirLock.b(AwDataDirLock.java:27)
at as0.i(as0.java:30)
at Zr0.run(Zr0.java:2)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:224)
at android.app.ActivityThread.main(ActivityThread.java:7520)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950)
解決方案
官方提供方案:WebView.setDataDirectorySuffix(suffix);
protected void attachBaseContext(Context base) {
mApplicationContext = base;
webViewSetPath(this);
}
public void webViewSetPath(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
String processName = SpecialUtils.getCurProcessName(context);
if(!CommonConstant.NEW_PACKAGE_NAME.equals(processName)){
WebView.setDataDirectorySuffix(getString(processName,"這里隱藏名字,自己設(shè)置個(gè)目錄"));
}
}
}
public String getString(String processName, String defValue) {
return TextUtils.isEmpty(processName) ? defValue : processName;
}
遇到的坑
實(shí)際在項(xiàng)目中運(yùn)用 application中設(shè)置多個(gè)存儲(chǔ)目錄,雖然能減少問題發(fā)生的次數(shù),但是我們?cè)趂irebase上依然能發(fā)現(xiàn)很多這個(gè)同樣的崩潰信息
那么這個(gè)問題發(fā)生的原因究竟是什么?一起來分析下拋出這個(gè)異常的邏輯吧
https://chromium.googlesource.com/chromium/src/+/refs/heads/main/android_webview/java/src/org/chromium/android_webview/AwDataDirLock.java#126
- // Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.android_webview;
import android.content.Context;
import android.os.Build;
import android.os.Process;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.PathUtils;
import org.chromium.base.StrictModeContext;
import org.chromium.base.metrics.ScopedSysTraceEvent;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
/**
* Handles locking the WebView's data directory, to prevent concurrent use from
* more than one process.
*/
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(")");
...
...
} catch (IOException e) {
error.append(" unknown");
}
return error.toString();
}
}
判斷原理:進(jìn)程是否持有WebView數(shù)據(jù)目錄中的webview_data.lock文件的鎖,如果子進(jìn)程也對(duì)相同文件嘗試加速,則會(huì)crash
- lock方法會(huì)對(duì)webview數(shù)據(jù)目錄中的webview_data.lock文件在for循環(huán)中嘗試加鎖16次
- 如果加鎖成功會(huì)將該進(jìn)程id和進(jìn)程名寫入到文件,如果加鎖失敗則會(huì)拋出異常
最佳解決方案
通過檢查目標(biāo)目錄的文件鎖,如果能夠獲得到鎖,就表明無異常;如果獲取不到文件鎖,再次重新設(shè)置存儲(chǔ)目錄。
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Context;
import android.text.TextUtils;
import android.webkit.WebView;
import java.io.File;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.util.List;
public class WebViewUtil {
public static void handleWebViewDir(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return;
}
try {
String suffix = "";
String processName = getCurProcessName(context);
if (!TextUtils.equals(context.getPackageName(), processName)) {//判斷不等于默認(rèn)進(jìn)程名稱
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();
}
}
public static String getCurProcessName(Context context) {
int pid = android.os.Process.myPid();
ActivityManager activityManager = (ActivityManager) context
.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager
.getRunningAppProcesses();
if (appProcesses == null) {
return null;
}
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
if (appProcess == null) {
continue;
}
if (appProcess.pid == pid) {
return appProcess.processName;
}
}
return null;
}
}