WebView 是个占内存大户,它和图片是OOM的两大元凶,图片一般使用三方库来加载,统一管理app中图片的内存,那么 WebView 呢,对于它又怎么处理?一般情况都是在 Activity 中使用到它时,动态的 new 一个webview,然后通过代码 addView() 方法添加到容器中,在 Activity 销毁时,把它从容器中 removeView() 掉,同时 load() 个空字符串,以减少内存的泄漏。
app可以有多个进程,一个主进程,其它的属于子进程,每个进程的内存上限是固定的,理论上进程越多,可以承受的内存就越大。但多进程也有坏处,由于进程之间的数据不能直接通用,此时单例等数据就会失效,进程间通信被称之为跨进程,跨进程有几种方案,之前的博客介绍过binder机制,通过aidl来实现跨进程通讯。多进程有利有弊,综合考量下来,把webview单独放到一个进程中,是很有必要的。还有个好处是,如果子进程崩溃了,主进程依然能继续运行,不会导致App崩溃。
把webview放到一个单独的进程中比较容易,我们可以用Fragment来封装一个WebView,然后用一个Activity来承载该Fragment,只需要在配置清单中,在此Activity的注册的节点中,重写 android:process 属性即可,我们可以自己定义名字,这样就跨进程了。
比如说主进程为A,子进程为B,之前讲解aidl时,是在主进程中注册Service服务,把Service定义为另外一个进程,通过定义aidl,绑定Binder来实现数据的通讯,在Activity中把数据传递给Service,然后获取到service返回的值,在Activity中进行下一步的操作;Service也可以是其他app的,也是通过aidl来进行通讯,常见的就是在自己的app中接入支付宝或微信支付,也是跨进程一通操作。回到WebView独立进程的话题中,此时主进程A和WebView进程B交互,也用binder机制,但是要注意一点,此时是在子进程B中来注册绑定Service与主进程A交互,Service是属于主进程的,这里和之前用aidl交互是颠倒的,这里想通了,后续操作就简单了。
interface IWebInterface {
String handleWeb(String name, String jsonParams);
}
定义了上面的aidl,到此,算是完成了三分之一,看似子进程B和主进程A可以交互了,但这里存在些问题,如果交互是耗时的,是不是每次子进程给主进程传递数据并等待数据返回时,都要开启一个线程?这样的操作怪怪的,有没有其它方法?aidl 也是有接口回调概念的,跨进程可以传递的除了数据基本类型,还有序列化对象,甚至还有接口,那么,我们可以对上面的做个改进,定义个接口,然后把接口作为个参数传递进去,
interface IWebCallback {
void onResult(int code, String name, String response);
}
interface IWebInterface {
void handleWeb(String name, String jsonParams, IWebCallback callback);
}
在AS的 aidl 文件夹下,创建这两个aidl文件,AS会自动替我们创建好对应的类;通过Context来调用 bindService() 方法时,有些注意事项,如果绑定后服务断开了,我们需要重新绑定,IBinder 有个linkToDeath() 方法给我们监听,当binder断开了会触发此回调,我们可以在这个方法中主动断开然后重连;还有个要注意的事是阻塞问题,bindService() 方法是异步的,为了保证绑定成功后再执行其他的逻辑,我们可以借助并发工具类 CountDownLatch,先写个简单的绑定例子
/**
* 这里是主进程
*/
public class MainProAidlInterface extends IWebInterface.Stub {
private Context context;
public MainProAidlInterface(Context context) {
this.context = context;
}
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
}
@Override
public void handleWeb(String name, String jsonParams, IWebCallback callback) throws RemoteException {
try {
handleRemoteAction(name, jsonParams, callback);
} catch (Exception e) {
e.printStackTrace();
}
}
private void handleRemoteAction(final String name, String params, final IWebCallback callback) throws Exception {
if(!TextUtils.isEmpty(name) && name.length() > 1){
if(callback != null){
callback.onResult(1, name, name +"," + params);
}
}
}
}
public class MainProHandleRemoteService extends Service {
private Context context;
@Override
public void onCreate() {
super.onCreate();
context = this;
}
@Override
public IBinder onBind(Intent intent) {
int pid = android.os.Process.myPid();
Binder binder = new MainProAidlInterface(context);
return binder;
}
}
public class RemoteWebBinder {
private Context mContext;
private IWebInterface mWebBinder;
private static volatile RemoteWebBinder sInstance;
private CountDownLatch mConnectBinderPoolCountDownLatch;
public RemoteWebBinder(Context context) {
this.mContext = context;
connectBinderService();
}
public static RemoteWebBinder getInstance(Context context) {
if (sInstance == null) {
synchronized (RemoteWebBinder.class) {
if (sInstance == null) {
sInstance = new RemoteWebBinder(context);
}
}
}
return sInstance;
}
private void connectBinderService() {
mConnectBinderPoolCountDownLatch = new CountDownLatch(1);
Intent service = new Intent(mContext, MainProHandleRemoteService.class);
mContext.bindService(service, mBinderConnection, Context.BIND_AUTO_CREATE);
try {
mConnectBinderPoolCountDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private ServiceConnection mBinderConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mWebBinder = IWebInterface.Stub.asInterface(service);
try {
mWebBinder.asBinder().linkToDeath(mBinderDeathRecipient, 0);
} catch (RemoteException e) {
e.printStackTrace();
}
mConnectBinderPoolCountDownLatch.countDown();
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
private IBinder.DeathRecipient mBinderDeathRecipient = new IBinder.DeathRecipient() { // 6
@Override
public void binderDied() {
mWebBinder.asBinder().unlinkToDeath(this, 0);
mWebBinder = null;
connectBinderService();
}
};
public IBinder getBinder(){
return mWebBinder.asBinder();
}
public IWebInterface getIWebInterface(){
return mWebBinder;
}
}
这里显示的是绑定Binder,跨进程就是通过它传递数据的,那再看看webview,这里写个例子,自己找了个html,然后稍加修改,把它放到 assets 文件夹中,命名为 wxremote.html
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<script type="text/javascript">
function actionFromNative(){
document.getElementById("log_msg").innerHTML +=
"<br\>Native调用了js函数";
}
function actionFromNativeWithParam(arg){
document.getElementById("log_msg").innerHTML +=
("<br\>Native调用了js函数并传递参数:"+arg);
}
</script>
</head>
<body>
<p>WebView与Javascript交互</p>
<div>
<button onClick="window.wx.post('1','come Js')">点击调用Native代码</button>
</div>
<br/>
<div>
<button onClick="window.wx.post('11','come from Js')">点击调用Native代码并传递参数</button>
</div>
<br/>
<br/>
<div id="log_msg">调用打印信息</div>
</body>
然后定义一个 WebView,做简单的封装
public class DWebView extends WebView {
public DWebView(Context context) {
this(context, null);
}
public DWebView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
protected Context context;
private DWebViewCallBack dWebViewCallBack;
private void init(Context context){
this.context = context;
addJavascriptInterface(this, "wx");
getSettings().setJavaScriptEnabled(true);
}
@JavascriptInterface
public void post(final String cmd, final String param) {
post(new Runnable() {
@Override
public void run() {
if(dWebViewCallBack != null){
dWebViewCallBack.exec(context, cmd, param, DWebView.this);
}
}
});
}
@JavascriptInterface
public void post(final String name) {
post(new Runnable() {
@Override
public void run() {
if(dWebViewCallBack != null){
dWebViewCallBack.exec(context, name, "", DWebView.this);
}
}
});
}
public void handleCallback(String response) {
if (!TextUtils.isEmpty(response)) {
String trigger = "javascript:" + "actionFromNativeWithParam" + "('" + response + "')";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
evaluateJavascript(trigger, null);
} else {
loadUrl(trigger);
}
}
}
public void setWebViewCallBack(DWebViewCallBack wWebViewCallBack) {
this.dWebViewCallBack = wWebViewCallBack;
}
public interface DWebViewCallBack {
void exec(Context context, String name, String params, WebView webView);
}
}
然后就是定义一个 Activity,这里先假设它是在主进程中,
public class WebRemoteActivity extends Activity implements DWebViewCallBack {
DWebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_web_remote);
webView = (DWebView) findViewById(R.id.webview);
webView.setWebViewCallBack(this);
loadUrl();
}
protected void loadUrl() {
webView.loadUrl("file:///android_asset/wxremote.html");
}
@Override
public void exec(Context context, String name, String params, WebView webView) {
CommandDispatcher.getInstance().exec(context, name, params, webView);
}
}
activity_web_remote:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="30dp"
android:orientation="vertical">
<remote.DWebView
android:id="@+id/webview"
android:layout_width="wrap_content"
android:layout_height="300dp"
/>
</LinearLayout>
DWebView 和 WebRemoteActivity 通过定义的接口 DWebViewCallBack 产生了关联,webview中的按钮被点击后,会执行注入的 post() 方法,然后通过回调执行到Activity中的方法;如果此时我们在配置清单中 WebRemoteActivity 的节点中重写 android:process 属性,则可以把它变为子进程,此时,如何跨进程?这时候再写个中间类,做事件的分发,同时判断是否需要跨进程或直接在子进程中执行逻辑。
此时就需要在Binder注册好之后再执行 loadUrl() 方法,并且webview调用原生的 exec() 方法,要在这个里面进行判断,是否需要跨进程,如果需要,则通过IWebInterface来传输数据,然后我们在主进程中做相应的数据处理,或保存到内存或本地,或上报接口等操作,数据处理完了,就触发回调,把数据等相关信息重新传递给子进程,子进程接到回调后,继续执行自己的逻辑,通知webview做下一步操作。
public class CommandDispatcher {
private final static String TAG = "CommandDispatcher";
private static CommandDispatcher instance;
private Handler mHandler = new Handler(Looper.getMainLooper());
// 实现跨进程通信的接口
protected IWebInterface webAidlInterface;
private CommandDispatcher() {
}
public static CommandDispatcher getInstance() {
if (instance == null) {
synchronized (CommandDispatcher.class) {
if (instance == null) {
instance = new CommandDispatcher();
}
}
}
return instance;
}
public void initAidlConnect(final Context context, final Action action) {
if (webAidlInterface != null) {
if (action != null) {
action.call(null);
}
return;
}
new Thread(new Runnable() {
@Override
public void run() {
RemoteWebBinder binderPool = RemoteWebBinder.getInstance(context);
webAidlInterface = binderPool.getIWebInterface();
if (action != null) {
action.call(null);
}
}
}).start();
}
public void exec(Context context, String name, String params, WebView webView){
try {
execNonUI(context, name, params, webView);
} catch (Exception e) {
e.printStackTrace();
}
}
private void execNonUI(Context context, String name, String params, final WebView webView) throws Exception {
if (inMainProcess(context, android.os.Process.myPid()) || name.length() < 2) {
handleCallback(0, name, params, webView);
} else {
if (webAidlInterface != null) {
webAidlInterface.handleWeb(name, params, new IWebCallback.Stub() {
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws
RemoteException {
}
@Override
public void onResult(int responseCode, String actionName, String response) throws RemoteException {
handleCallback(responseCode, actionName, response, webView);
}
});
}
}
}
private void handleCallback(final int responseCode, final String name, final String response,
final WebView webView) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (webView instanceof DWebView) {
((DWebView) webView).handleCallback(response);
}
}
});
}
public static boolean inMainProcess(Context context, int pid) {
String packageName = context.getPackageName();
String processName = getProcessName(context,pid);
return packageName.equals(processName);
}
public static String getProcessName(Context context, int pid) {
// ActivityManager
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> runningApps = am.getRunningAppProcesses();
if (runningApps == null) {
return null;
}
for (ActivityManager.RunningAppProcessInfo procInfo : runningApps) {
if (procInfo.pid == pid) {
return procInfo.processName;
}
}
return null;
}
public void runOnUiThread(Runnable runnable){
mHandler.post(runnable);
}
}
execNonUI() 方法中做了简单的逻辑区分,实际中比这里要复杂,可以定义好规则,比如以remote开头的方法名需要跨进程,这个还是根据业务需要定义的。以上就是个很简单粗糙的例子,实际使用中还是需要逐步完善的。