第二篇问题整理,主要涉及 WebView 的内存管理和 Cookie 同步,以及一些其他细节。

WebView 内存泄漏

不要在 XML 中直接声明 WebView,因为 Activity 销毁后 WebView 仍可能持有 Context 引用,导致内存无法释放。正确的做法:

使用 ApplicationContext

1
WebView webView = new WebView(getApplicationContext());

在 Fragment 中正确处理生命周期

1
2
3
4
5
6
@Override
public void onDetach() {
    super.onDetach();
    webView.removeAllViews();
    webView.destroy();
}

进程管理:AndroidManifest 中的 process 属性

可在清单文件中为不同组件分配独立进程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<application
    android:process="com.processkill.p1">
    <activity
        android:name="com.processkill.A"
        android:process="com.processkill.p2">
    </activity>
    <activity
        android:name="com.processkill.B"
        android:process="com.processkill.p3">
    </activity>
</application>

避免静态 Drawable 导致的内存泄漏

Romain Guy 写过一篇经典文章 Avoid Memory Leaks on Android。虽然现在看来有些过时——从 Android 4.0.1 开始,Drawable.setCallback() 已经改用 WeakReference——但使用 static 关键字持有 Drawable 仍然是不好的实践。

Android 框架内部在设置新背景时也会清理前一个引用:

1
2
3
4
5
6
7
8
/*
 * Regardless of whether we're setting a new background or not, we want
 * to clear the previous drawable.
 */
if (mBackground != null) {
    mBackground.setCallback(null);
    unscheduleDrawable(mBackground);
}

WebView URL 参数编码

在向链接追加参数时,需要对参数值进行编码。以前使用 NameValuePair(API 23 起已标记 @deprecated),也可以自己实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
JSONObject json = new JSONObject();
Iterator<String> keys = json.keys();
StringBuilder stringBuilder = new StringBuilder();
try {
    while (keys.hasNext()) {
        String key = keys.next();
        String value = json.optString(key);
        if (value != null) {
            stringBuilder.append(URLEncoder.encode(key, "UTF-8"))
                    .append("=")
                    .append(URLEncoder.encode(value, "UTF-8"));
        }
    }
} catch (UnsupportedEncodingException e) {
    e.printStackTrace();
}

本质与 NameValuePair 相同——后者内部也是两次 toString 操作。

使用 OkHttpClient 时需要手动将 Cookie 同步到 WebView 的 CookieManager。抓包发现没有 Cookie,研究文档才发现问题所在。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Uri uri = API.getUri();

HttpUrl httpUrl = new HttpUrl.Builder()
        .scheme(uri.getScheme())
        .host(uri.getHost())
        .build();

OkHttpClient okHttpClient = ClientManager.getInstance();
CookieJar cookieJar = okHttpClient.cookieJar();

List<Cookie> cookies = cookieJar.loadForRequest(httpUrl);

for (Cookie cookie : cookies) {
    if (cookie != null) {
        String cookieString = cookie.name() + "=" + cookie.value() + "; domain=" + cookie.domain();
        cookieManager.setCookie(httpUrl.toString(), cookieString);
    }
}

关键点在于 CookieManager.setCookie() 方法的第一个参数接收的是 完整 URL,而不是单纯的 Host:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/**
 * Sets a cookie for the given URL. Any existing cookie with the same host,
 * path and name will be replaced with the new cookie. The cookie being set
 * will be ignored if it is expired.
 *
 * @param url the URL for which the cookie is to be set
 * @param value the cookie as a string, using the format of the 'Set-Cookie'
 *              HTTP response header
 */
public abstract void setCookie(String url, String value);

References