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. 源码分析

  调用入口:

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。

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 方法。好家伙,这个路是真长,继续往下看。

 

@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);
    }

......

}

 

废话不多说,继续往下。

 

@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 

// 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);
}
#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进程间通信是不是有点别扭,内心很矛盾。希望能够看到的童鞋,能够给我个解答。

posted @ 2020-10-28 21:23  Spiderman.L  阅读(2542)  评论(0编辑  收藏  举报