Android 解決WebView多進(jìn)程崩潰的方法
問題
在android 9.0系統(tǒng)上如果多個(gè)進(jìn)程使用WebView需要使用官方提供的api在子進(jìn)程中給webview的數(shù)據(jù)文件夾設(shè)置后綴:
WebView.setDataDirectorySuffix(suffix);
否則將會(huì)報(bào)出以下錯(cuò)誤:
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)
通過使用官方提供的方法后問題只減少了一部分,從bugly后臺(tái)依然能收到此問題的大量崩潰信息,以至于都沖上了崩潰問題Top3。
問題分析
從源碼分析調(diào)用鏈最終調(diào)用到了AwDataDirLock類中的lock方法。
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();
}
}
lock方法會(huì)對(duì)webview數(shù)據(jù)目錄中的webview_data.lock文件在for循環(huán)中嘗試加鎖16次,注釋中也說明了這么做的原因:可能出現(xiàn)的極端情況是一個(gè)舊進(jìn)程正在被殺死時(shí)一個(gè)新的進(jìn)程啟動(dòng)了,看來Google工程師對(duì)這個(gè)問題也很頭痛;如果加鎖成功會(huì)將該進(jìn)程id和進(jìn)程名寫入到文件,如果加鎖失敗則會(huì)拋出異常。所以在android9.0以上檢測(cè)應(yīng)用是否存在多進(jìn)程共用WebView數(shù)據(jù)目錄的原理就是進(jìn)程持有WebView數(shù)據(jù)目錄中的webview_data.lock文件的鎖。所以如果子進(jìn)程也對(duì)相同文件嘗試加鎖則會(huì)導(dǎo)致應(yīng)用崩潰。
解決方案
目前大部分手機(jī)會(huì)在應(yīng)用崩潰時(shí)自動(dòng)重啟應(yīng)用,猜測(cè)當(dāng)手機(jī)系統(tǒng)運(yùn)行較慢時(shí)這時(shí)就會(huì)出現(xiàn)注釋中提到的當(dāng)一個(gè)舊進(jìn)程正在被殺死時(shí)一個(gè)新的進(jìn)程啟動(dòng)了的情況。既然獲取文件鎖失敗就會(huì)發(fā)生崩潰,并且該文件只是用于加鎖判斷是否存在多進(jìn)程共用WebView數(shù)據(jù)目錄,每次加鎖成功都會(huì)重新寫入對(duì)應(yīng)進(jìn)程信息,那么我們可以在應(yīng)用啟動(dòng)時(shí)對(duì)該文件嘗試加鎖,如果加鎖失敗就刪除該文件并重新創(chuàng)建,加鎖成功就立即釋放鎖,這樣當(dāng)系統(tǒng)嘗試加鎖時(shí)理論上是可以加鎖成功的,也就避免了這個(gè)問題的發(fā)生。
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)) {//判斷不等于默認(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();
}
}
使用此方案應(yīng)用上線后該問題崩潰次數(shù)減少了90%以上。也許Google工程師應(yīng)該考慮下?lián)Q一種技術(shù)方案檢測(cè)應(yīng)用是否存在多進(jìn)程共用WebView數(shù)據(jù)目錄。
以上就是Android 解決WebView多進(jìn)程崩潰的方法的詳細(xì)內(nèi)容,更多關(guān)于Android 解決WebView多進(jìn)程崩潰的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android編程使用GestureDetector實(shí)現(xiàn)簡(jiǎn)單手勢(shì)監(jiān)聽與處理的方法
這篇文章主要介紹了Android編程使用GestureDetector實(shí)現(xiàn)簡(jiǎn)單手勢(shì)監(jiān)聽與處理的方法,簡(jiǎn)單講述了Android手勢(shì)監(jiān)聽的原理并結(jié)合實(shí)例形式分析了GestureDetector實(shí)現(xiàn)手勢(shì)監(jiān)聽與處理的相關(guān)操作技巧,需要的朋友可以參考下2017-09-09
Android RecyclerView 上拉加載更多及下拉刷新功能的實(shí)現(xiàn)方法
這篇文章主要介紹了Android RecyclerView 上拉加載更多及下拉刷新的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-09-09
Android AIDL實(shí)現(xiàn)跨進(jìn)程通信的示例代碼
本篇文章主要介紹了Android AIDL實(shí)現(xiàn)跨進(jìn)程通信的示例代碼,具有一定的參考價(jià)值,有興趣的可以了解一下2017-08-08
Android selector狀態(tài)選擇器的使用詳解
這篇文章主要為大家詳細(xì)介紹了Android selector狀態(tài)選擇器的使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-09-09
android教程使用webview訪問https的url處理sslerror示例
這篇文章主要介紹了android教程使用webview訪問https的url處理sslerror示例,大家參考使用吧2014-01-01
C/C++在Java、Android和Objective-C三大平臺(tái)下實(shí)現(xiàn)混合編程
本文主要介紹C/C++在Java、Android和Objective-C三大平臺(tái)下實(shí)現(xiàn)混合編程,這里舉例說明實(shí)現(xiàn)不同平臺(tái)用C/C++實(shí)現(xiàn)編程的方法,有興趣的小伙伴可以參考下2016-08-08
Android使用ViewPager實(shí)現(xiàn)頂部tabbar切換界面
這篇文章主要為大家詳細(xì)介紹了使用ViewPager實(shí)現(xiàn)頂部tabbar切換界面,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
使用ViewPager實(shí)現(xiàn)android軟件使用向?qū)Чδ軐?shí)現(xiàn)步驟
現(xiàn)在的大部分android軟件,都是使用說明,就是第一次使用該軟件時(shí),會(huì)出現(xiàn)向?qū)?,可以左右滑?dòng),然后就進(jìn)入應(yīng)用的主界面了,下面我們就實(shí)現(xiàn)這個(gè)功能2013-11-11

