Android WebView 的 addJavascriptInterface 探究
一、前言
Java和JS交互的方式有多种,这里探讨的方式是通过以下方式进行的交互。
webView.addJavascriptInterface(this, "JSBridge")
这篇文章是想弄明白 JavaScript 和 Java是如何实现这种方式互调的,就从源码角度开始分析 。
二、分析
1. 图示调用关系
上面这张调用关系流程图,关于源码是基于Android4.4 源码进行分析,Android在4.4将WebView内核改为 chromium ,在Android4.4以上的系统源码会有所调整。在 翻阅Android 6.0的时候,源码中提示 :Building the Chromium-based WebView in AOSP is no longer supported. WebView can now be built entirely from the Chromium source code. 这个句话的意思是基于chromium 的WebView不再在安卓开放源代码项目,所以如果你在6.0以上的版本中将找不到下面即将分析的源代码。具体的调整你可以自行查阅。
2. 源码分析
- WebView 源码位置:/frameworks/base/core/java/android/webkit/WebView.java
调用入口:
private WebViewProvider mProvider;
/** * Injects the supplied Java object into this WebView. The object is * injected into the JavaScript context of the main frame, using the * supplied name. This allows the Java object's methods to be * accessed from JavaScript. */ public void addJavascriptInterface(Object object, String name) { checkThread(); mProvider.addJavascriptInterface(object, name); }
从源码可以看到实际调用为内部成员变量 WebViewProvider 的 addJavascriptInterface方法。继续深入发现 WebViewProvider 是一个接口,我们要找到具体的实现类。那么要看 mProvider 是怎么实例化的。
private static synchronized WebViewFactoryProvider getFactory() { return WebViewFactory.getProvider(); }
private void ensureProviderCreated() { checkThread(); if (mProvider == null) { // As this can get called during the base class constructor chain, pass the minimum // number of dependencies here; the rest are deferred to init(). mProvider = getFactory().createWebView(this, new PrivateAccess()); } }
通过源码可以知道实际创建 WebViewProvider 实例的是通过调用 WebViewFactory的 getProvider()方法,获取到 WebViewFactoryProvider 对象(注意和前者WebViewProvider的区别),然后再调用 WebViewFactoryProvider 的 createWebView 方法。那我们继续往下分析它的 getProvider() 方法。从这里也可以看出源码基本上都是基于接口编程,好处谁用谁知道。
-
WebViewFactory
private static final String CHROMIUM_WEBVIEW_FACTORY = "com.android.webview.chromium.WebViewChromiumFactoryProvider"; static WebViewFactoryProvider getProvider() { synchronized (sProviderLock) { // For now the main purpose of this function (and the factory abstraction) is to keep // us honest and minimize usage of WebView internals when binding the proxy. if (sProviderInstance != null) return sProviderInstance; Class<WebViewFactoryProvider> providerClass; try { providerClass = getFactoryClass(); } catch (ClassNotFoundException e) { Log.e(LOGTAG, "error loading provider", e); throw new AndroidRuntimeException(e); } // This implicitly loads Preloader even if it wasn't preloaded at boot. if (Preloader.sPreloadedProvider != null && Preloader.sPreloadedProvider.getClass() == providerClass) { sProviderInstance = Preloader.sPreloadedProvider; if (DEBUG) Log.v(LOGTAG, "Using preloaded provider: " + sProviderInstance); return sProviderInstance; } // The preloaded provider isn't the one we wanted; construct our own. StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); try { sProviderInstance = providerClass.newInstance(); if (DEBUG) Log.v(LOGTAG, "Loaded provider: " + sProviderInstance); return sProviderInstance; } catch (Exception e) { Log.e(LOGTAG, "error instantiating provider", e); throw new AndroidRuntimeException(e); } finally { StrictMode.setThreadPolicy(oldPolicy); } } } private static Class<WebViewFactoryProvider> getFactoryClass() throws ClassNotFoundException { return (Class<WebViewFactoryProvider>) Class.forName(CHROMIUM_WEBVIEW_FACTORY); }
上面标红的即为关键代码,通过源码我们知道 WebViewFactoryProvider 是通过反射 "com.android.webview.chromium.WebViewChromiumFactoryProvider" 创建的,那么我们需要找到 WebViewChromiumFactoryProvider 这个类,并且找到它的 createWebView 方法,看看里面做了什么,使用什么实例化了上面的 mProvider。
-
WebViewChromiumFactoryProvider 源码位置:/frameworks/webview/chromium/java/com/android/webview/chromium/WebViewChromiumFactoryProvider.java
public class WebViewChromiumFactoryProvider implements WebViewFactoryProvider { ....
@Override public WebViewProvider createWebView(WebView webView, WebView.PrivateAccess privateAccess) { WebViewChromium wvc = new WebViewChromium(this, webView, privateAccess); synchronized (mLock) { if (mWebViewsToStart != null) { mWebViewsToStart.add(new WeakReference<WebViewChromium>(wvc)); } } ResourceProvider.registerResources(webView.getContext()); return wvc; }
... }
看得出 mProvider 就是引用了 WebViewChromium 实例,也就是说在一开始入口处 mProvider.addJavascriptInterface(object, name) 就是调用 WebViewChromium 的 addJavascriptInterface 。终于找到了本尊,哈哈,兜兜转转就是为了抽象。接下来我们就看WebViewChromium
- WebViewChromium
class WebViewChromium implements WebViewProvider, WebViewProvider.ScrollDelegate, WebViewProvider.ViewDelegate { ...... // The WebView wrapper for ContentViewCore and required browser compontents. private AwContents mAwContents; ...... private void initForReal() { mAwContents = new AwContents(mFactory.getBrowserContext(), mWebView, new InternalAccessAdapter(), mContentsClientAdapter, new AwLayoutSizer(), mWebSettings.getAwSettings()); if (mAppTargetSdkVersion >= Build.VERSION_CODES.KITKAT) { // On KK and above, favicons are automatically downloaded as the method // old apps use to enable that behavior is deprecated. AwContents.setShouldDownloadFavicons(); } } ...... @Override public void loadUrl(final String url, Map<String, String> additionalHttpHeaders) { // TODO: We may actually want to do some sanity checks here (like filter about://chrome). // For backwards compatibility, apps targeting less than K will have JS URLs evaluated // directly and any result of the evaluation will not replace the current page content. // Matching Chrome behavior more closely; apps targetting >= K that load a JS URL will // have the result of that URL replace the content of the current page. final String JAVASCRIPT_SCHEME = "javascript:"; if (mAppTargetSdkVersion < Build.VERSION_CODES.KITKAT && url != null && url.startsWith(JAVASCRIPT_SCHEME)) { mFactory.startYourEngines(true); if (checkNeedsPost()) { mRunQueue.addTask(new Runnable() { @Override public void run() { mAwContents.evaluateJavaScriptEvenIfNotYetNavigated( url.substring(JAVASCRIPT_SCHEME.length())); } }); } else { mAwContents.evaluateJavaScriptEvenIfNotYetNavigated( url.substring(JAVASCRIPT_SCHEME.length())); } return; } LoadUrlParams params = new LoadUrlParams(url); if (additionalHttpHeaders != null) params.setExtraHeaders(additionalHttpHeaders); loadUrlOnUiThread(params); } ...... @Override public void addJavascriptInterface(final Object obj, final String interfaceName) { if (checkNeedsPost()) { mRunQueue.addTask(new Runnable() { @Override public void run() { addJavascriptInterface(obj, interfaceName); } }); return; } Class<? extends Annotation> requiredAnnotation = null; if (mAppTargetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN_MR1) { //SDK 17 android 4.2 requiredAnnotation = JavascriptInterface.class; } mAwContents.addPossiblyUnsafeJavascriptInterface(obj, interfaceName, requiredAnnotation); } ...... }
源码中我保留了 loadUrl ,就是为了引起你的注意,你能探索的不止这一点点。源码中高亮标红的代码,是我们特别关注的。 WebViewChromium 的 addJavascriptInterface 先做了一个版本判断,判断是否需要 JavascriptInterface 注解标记。然后调用了 AwContents 的addPossiblyUnsafeJavascriptInterface 方法。好家伙,这个路是真长,继续往下看。
-
AwContents 源码位置:/external/chromium_org/android_webview/java/src/org/chromium/android_webview/AwContents.java
@JNINamespace("android_webview") public class AwContents { ...... private ContentViewCore mContentViewCore; ...... /** * @see ContentViewCore#addPossiblyUnsafeJavascriptInterface(Object, String, Class) */ public void addPossiblyUnsafeJavascriptInterface(Object object, String name, Class<? extends Annotation> requiredAnnotation) { mContentViewCore.addPossiblyUnsafeJavascriptInterface(object, name, requiredAnnotation); } ...... }
废话不多说,继续往下。
-
ContentViewCore 源码位置:/external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java
@JNINamespace("content") public class ContentViewCore implements MotionEventDelegate, NavigationClient, AccessibilityStateChangeListener { ...... /** * This method injects the supplied Java object into the ContentViewCore. * The object is injected into the JavaScript context of the main frame, * using the supplied name. This allows the Java object to be accessed from * JavaScript. Note that that injected objects will not appear in * JavaScript until the page is next (re)loaded. For example: * <pre> view.addJavascriptInterface(new Object(), "injectedObject"); * view.loadData("<!DOCTYPE html><title></title>", "text/html", null); * view.loadUrl("javascript:alert(injectedObject.toString())");</pre> * <p><strong>IMPORTANT:</strong> * <ul> * <li> addJavascriptInterface() can be used to allow JavaScript to control * the host application. This is a powerful feature, but also presents a * security risk. Use of this method in a ContentViewCore containing * untrusted content could allow an attacker to manipulate the host * application in unintended ways, executing Java code with the permissions * of the host application. Use extreme care when using this method in a * ContentViewCore which could contain untrusted content. Particular care * should be taken to avoid unintentional access to inherited methods, such * as {@link Object#getClass()}. To prevent access to inherited methods, * pass an annotation for {@code requiredAnnotation}. This will ensure * that only methods with {@code requiredAnnotation} are exposed to the * Javascript layer. {@code requiredAnnotation} will be passed to all * subsequently injected Java objects if any methods return an object. This * means the same restrictions (or lack thereof) will apply. Alternatively, * {@link #addJavascriptInterface(Object, String)} can be called, which * automatically uses the {@link JavascriptInterface} annotation. * <li> JavaScript interacts with Java objects on a private, background * thread of the ContentViewCore. Care is therefore required to maintain * thread safety.</li> * </ul></p> * * @param object The Java object to inject into the * ContentViewCore's JavaScript context. Null * values are ignored. * @param name The name used to expose the instance in * JavaScript. * @param requiredAnnotation Restrict exposed methods to ones with this * annotation. If {@code null} all methods are * exposed. * */ public void addPossiblyUnsafeJavascriptInterface(Object object, String name, Class<? extends Annotation> requiredAnnotation) { if (mNativeContentViewCore != 0 && object != null) { mJavaScriptInterfaces.put(name, object); nativeAddJavascriptInterface(mNativeContentViewCore, object, name, requiredAnnotation, mRetainedJavaScriptObjects); } } ......
private native void nativeAddJavascriptInterface(int nativeContentViewCoreImpl, Object object, String name, Class requiredAnnotation, HashSet<Object> retainedObjectSet);
......
}
恭喜,我们走完了Java层的代码,现在我们要去native层了。我们要找到对应的C代码。
-
content_view_core_impl.cc 源码位置:/external/chromium_org/content/browser/android/content_view_core_impl.cc
namespace content { namespace { ...... // Enables a callback when the underlying WebContents is destroyed, to enable // nulling the back-pointer. class ContentViewCoreImpl::ContentViewUserData : public base::SupportsUserData::Data { public: explicit ContentViewUserData(ContentViewCoreImpl* content_view_core) : content_view_core_(content_view_core) {} } ...... void ContentViewCoreImpl::AddJavascriptInterface( JNIEnv* env, jobject /* obj */, jobject object, jstring name, jclass safe_annotation_clazz, jobject retained_object_set) { ScopedJavaLocalRef<jobject> scoped_object(env, object); ScopedJavaLocalRef<jclass> scoped_clazz(env, safe_annotation_clazz); JavaObjectWeakGlobalRef weak_retained_object_set(env, retained_object_set); // JavaBoundObject creates the NPObject with a ref count of 1, and // JavaBridgeDispatcherHostManager takes its own ref. JavaBridgeDispatcherHostManager* java_bridge = web_contents_->java_bridge_dispatcher_host_manager(); java_bridge->SetRetainedObjectSet(weak_retained_object_set); NPObject* bound_object = JavaBoundObject::Create(scoped_object, scoped_clazz, java_bridge->AsWeakPtr()); java_bridge->AddNamedObject(ConvertJavaStringToUTF16(env, name), bound_object); WebKit::WebBindings::releaseObject(bound_object); } ...... }
ContentViewCoreImpl::AddJavascriptInterface 就是我们要找的Native代码,看标注我们需要继续看 JavaBridgeDispatcherHostManager。
-
JavaBridgeDispatcherHostManager 源码位置:/external/chromium_org/content/browser/renderer_host/java/java_bridge_dispatcher_host_manager.cc
namespace content { JavaBridgeDispatcherHostManager::JavaBridgeDispatcherHostManager( WebContents* web_contents) : WebContentsObserver(web_contents) { } JavaBridgeDispatcherHostManager::~JavaBridgeDispatcherHostManager() { for (ObjectMap::iterator iter = objects_.begin(); iter != objects_.end(); ++iter) { WebKit::WebBindings::releaseObject(iter->second); } DCHECK_EQ(0U, instances_.size()); } void JavaBridgeDispatcherHostManager::AddNamedObject(const string16& name, NPObject* object) { // Record this object in a map so that we can add it into RenderViewHosts // created later. The JavaBridgeDispatcherHost instances will take a // reference to the object, but we take one too, because this method can be // called before there are any such instances. WebKit::WebBindings::retainObject(object); objects_[name] = object; for (InstanceMap::iterator iter = instances_.begin(); iter != instances_.end(); ++iter) { iter->second->AddNamedObject(name, object); } }
配合上面的代码,需要附上 java_bridge_dispatcher_host_manager.h 对 objects_ 以及 instances_ 的声明
namespace content { class JavaBridgeDispatcherHost; class RenderViewHost; // This class handles injecting Java objects into all of the RenderViews // associated with a WebContents. It manages a set of JavaBridgeDispatcherHost // objects, one per RenderViewHost. class JavaBridgeDispatcherHostManager : public WebContentsObserver, public base::SupportsWeakPtr<JavaBridgeDispatcherHostManager> { ...... private: typedef std::map<RenderViewHost*, scoped_refptr<JavaBridgeDispatcherHost> > InstanceMap; InstanceMap instances_; typedef std::map<string16, NPObject*> ObjectMap; ObjectMap objects_; JavaObjectWeakGlobalRef retained_object_set_; ...... }
结合.h和.c的代码,ContentViewCoreImpl::AddJavascriptInterface中关于 instances_的遍历,遍历的对象是 JavaBridgeDispatcherHost,并调用其 AddNamedObject 方法。好,继续往下。
#if !defined(OS_ANDROID) #error "JavaBridge currently only supports OS_ANDROID" #endif
namespace content { namespace { // The JavaBridge needs to use a Java thread so the callback // will happen on a thread with a prepared Looper. class JavaBridgeThread : public base::android::JavaHandlerThread { public: JavaBridgeThread() : base::android::JavaHandlerThread("JavaBridge") { Start(); } virtual ~JavaBridgeThread() { Stop(); } }; ...... void JavaBridgeDispatcherHost::AddNamedObject(const string16& name, NPObject* object) { NPVariant_Param variant_param; CreateNPVariantParam(object, &variant_param); if (!is_renderer_initialized_) { is_renderer_initialized_ = true; Send(new JavaBridgeMsg_Init(routing_id())); } Send(new JavaBridgeMsg_AddNamedObject(routing_id(), name, variant_param)); } ...... bool JavaBridgeDispatcherHost::Send(IPC::Message* msg) { return RenderViewHostObserver::Send(msg); } ...... } }
哈哈,看到这露出了姨母笑。IPC ,嘿嘿嘿。继续看 RenderViewHostObserver 。源码中有一句话,JavaBridge currently only supports OS_ANDROID
- RenderViewHostObserver 源码位置:/external/chromium_org/content/public/browser/render_view_host_observer.cc
// render_view_host_observer.h RenderViewHostImpl* render_view_host_; namespace content { RenderViewHostObserver::RenderViewHostObserver(RenderViewHost* render_view_host) : render_view_host_(static_cast<RenderViewHostImpl*>(render_view_host)), routing_id_(render_view_host_->GetRoutingID()) { ...... bool RenderViewHostObserver::Send(IPC::Message* message) { if (!render_view_host_) { delete message; return false; } return render_view_host_->Send(message); } ...... } }
- RenderViewHostImpl 发现该类中并没有 Send 方法,然后查看其父类是 RenderWidgetHostImpl ,然后查看 RenderWidgetHostImpl 中是否有 Send 方法。
- RenderWidgetHostImpl
// .h // Created during construction but initialized during Init*(). Therefore, it // is guaranteed never to be NULL, but its channel may be NULL if the // renderer crashed, so you must always check that. RenderProcessHost* process_; //.c bool RenderWidgetHostImpl::Send(IPC::Message* msg) { if (IPC_MESSAGE_ID_CLASS(msg->type()) == InputMsgStart) return input_router_->SendInput(msg); return process_->Send(msg); }
-
RenderProcessHost 该类也没有Send方法,查看其父类 IPC::Sender
- IPC::Sender 源码位置:/external/chromium_org/ipc/ipc_sender.h
#ifndef IPC_IPC_SENDER_H_ #define IPC_IPC_SENDER_H_ #include "ipc/ipc_export.h" namespace IPC { class Message; class IPC_EXPORT Sender { public: // Sends the given IPC message. The implementor takes ownership of the // given Message regardless of whether or not this method succeeds. This // is done to make this method easier to use. Returns true on success and // false otherwise. virtual bool Send(Message* msg) = 0; protected: virtual ~Sender() {} }; } // namespace IPC #endif // IPC_IPC_SENDER_H_
走到这里就是终点了。可以看出,开始的 WebView addJavascriptInterface 方法调用,其实就是通过 chromium 的IPC 机制发送了一条消息出去。chromium 的IPC 是通过 unix socket 进行通信的。那这个消息发送给谁,消息是如何发送、接收的,这个可以看下面两篇罗老师的文章。
这里有一个自己的疑惑,第一个点:chrome浏览器是多进程架构,Chromium for Android WebView 是单进程架构。第二个点:上面分析到的IPC的通信是进程间通信。单进程架构和IPC进程间通信是不是有点别扭,内心很矛盾。希望能够看到的童鞋,能够给我个解答。