做了一段时间的WebKit开发,后来又研究了一下WebKit的插件,但一直没有时间总结一下,现在终于有点时间可以梳理一下了,也希望能跟大家多多交流
首先要说明一下什么是WebKit插件,WebKit插件就是内核是WebKit的浏览器的插件,IE的不知道,但firefox的插件应该与WebKit插件类似,因为WebKit插件使用的主要一个技术或者说接口叫做NPAPI,而这套接口在mozilla的网站上面有详细的介绍,各位有兴趣可以去mozilla看看。回过头来,啥叫浏览器插件呢?或者说为啥要有这么个东西存在?我的理解是为了扩展浏览器的功能,大家知道,浏览器的功能主要是渲染网页,网页主要是由html,javascript,css组成,另外还包括一些资源文件,如果只是渲染网页,浏览器一般够用了,但随着网页内容的不断多样化,复杂化,比如多媒体文件的播放,浏览器已经不能提供这些功能了,于是插件就出现了,其中最有代表性的就是flash了。
插件的加载过程简单的说就是:
- 浏览器在解析网页的时候,发现某个节点声明了某个MIME类型
- 根据这个MIME类型查找注册过的插件,找到了就加载该插件
具体的说,在Android上面这个过程如下:
- 当发现网页上存在object节点的时候,WebKit将进入加载plug-in的流程
- 查找所有Intent类型为PLUGIN_ACTION的service,返回所有service的插件lib路径,形如/data/data/com.android.sampleplugin/lib(注:可能还会搜索/system/lib/下面的so)
- 遍历各个目录中的动态库.so文件,通过预先定义的entryPoint解析出插件的信息,包括名字,描述,mime类型等,并保存这些信息
- 整个WebView在layout的时候会创建plug-in,在这个过程中根据mime类型等创建PluginView(插件视图?)
- 接着会建立PluginPackage,建立的过程中会调用NP_Initialize,完成Plugin的初始化工作
注:在3中建立PluginView的时候会找到对应mime类型的PluginPackage(mime和PluginPackage一一对应),这样在PluginView的start里面会利用PluginPackage的接口调用NPP_New,创建plugin的实例(一个PluginView和一个plugin的实例一一对应),并且同时创建一个对应的NPObject的实例,记录在NPP的pdata里面(一个plugin的实例对应一个NPObject的实例)
在Android平台开发插件,下面这4个函数是必须实现的:
NPError NP_Initialize(NPNetscapeFuncs* browserFuncs, NPPluginFuncs* pluginFuncs, void *java_env);
NPError NP_GetValue(NPP instance, NPPVariable variable, void *value);
const char* NP_GetMIMEDescription(void);
void NP_Shutdown(void);
注:这个在不同的平台下面有区别,我发现在mac os上面需要实现
NPError NP_Initialize(NPNetscapeFuncs *browserFuncs);
NPError NP_GetEntryPoints(NPPluginFuncs *pluginFuncs);
void NP_Shutdown(void);
这3个函数,比较一下参数就知道,Android下面的
NP_Initialize
包括了mac os中的
NP_Initialize
和
NP_GetEntryPoints
的功能,额外的java_env是将java虚拟机环境传下来,作为jni层的context,这个是Android必须要的,你懂的
但mac os为啥不需要实现
NP_GetMIMEDescription
呢?嗯,这个俺没去深究,有谁有兴趣的可以去研究一下
下面看看
const char* NP_GetMIMEDescription(void);
比如返回:"application/x-yourname:tst:MIME type is application/x-yourname"
可以参考这里。
接着看看
NP_Initialize
比较重要,通过browserFuncs将浏览器的接口传给插件,通过pluginFuncs得到插件的接口,java_env上面提过了
问题:为啥用这种方式将接口传给插件呢?
一般来说,公布模块的接口,我们会直接声明函数的,要使用它,包含头文件,库文件就可以直接调用了,但这里有个问题,我们开发的是插件,是给浏览器用的,而不是直接调用浏览器。从语法上说明可能更清楚一些,在Android上面这些插件都是app,并包含动态库.so文件,按照一般的调用方式,将提示找不到这些接口的实现,是的,用函数指针将解决这个问题,在插件中访问浏览器的接口都是通过函数指针实现的,感觉有点类似C++中的虚函数,万能的指针,C的灵魂啊
问题:那为啥插件的接口传给浏览器也要用这种方式呢?
对啊,既然调用是从浏览器到插件的,那插件直接提供头文件,浏览器直接调用不就好了,为啥又要用函数指针?这个我也没整明白,貌似用直接调用的方式也是可行的,比如我们在讨论的这4个函数就是从浏览器直接调用插件的,不知道大家有没有别的答案。
下面分析一下参数browserFuncs,在npfunctions.h中定义的:
typedef struct _NPNetscapeFuncs {
uint16 size;
uint16 version;
NPN_GetURLProcPtr geturl;
NPN_PostURLProcPtr posturl;
NPN_RequestReadProcPtr requestread;
NPN_NewStreamProcPtr newstream;
NPN_WriteProcPtr write;
NPN_DestroyStreamProcPtr destroystream;
NPN_StatusProcPtr status;
NPN_UserAgentProcPtr uagent;
NPN_MemAllocProcPtr memalloc;
NPN_MemFreeProcPtr memfree;
NPN_MemFlushProcPtr memflush;
NPN_ReloadPluginsProcPtr reloadplugins;
NPN_GetJavaEnvProcPtr getJavaEnv;
NPN_GetJavaPeerProcPtr getJavaPeer;
NPN_GetURLNotifyProcPtr geturlnotify;
NPN_PostURLNotifyProcPtr posturlnotify;
NPN_GetValueProcPtr getvalue;
NPN_SetValueProcPtr setvalue;
NPN_InvalidateRectProcPtr invalidaterect;
NPN_InvalidateRegionProcPtr invalidateregion;
NPN_ForceRedrawProcPtr forceredraw;
NPN_GetStringIdentifierProcPtr getstringidentifier;
NPN_GetStringIdentifiersProcPtr getstringidentifiers;
NPN_GetIntIdentifierProcPtr getintidentifier;
NPN_IdentifierIsStringProcPtr identifierisstring;
NPN_UTF8FromIdentifierProcPtr utf8fromidentifier;
NPN_IntFromIdentifierProcPtr intfromidentifier;
NPN_CreateObjectProcPtr createobject;
NPN_RetainObjectProcPtr retainobject;
NPN_ReleaseObjectProcPtr releaseobject;
NPN_InvokeProcPtr invoke;
NPN_InvokeDefaultProcPtr invokeDefault;
NPN_EvaluateProcPtr evaluate;
NPN_GetPropertyProcPtr getproperty;
NPN_SetPropertyProcPtr setproperty;
NPN_RemovePropertyProcPtr removeproperty;
NPN_HasPropertyProcPtr hasproperty;
NPN_HasMethodProcPtr hasmethod;
NPN_ReleaseVariantValueProcPtr releasevariantvalue;
NPN_SetExceptionProcPtr setexception;
NPN_PushPopupsEnabledStateProcPtr pushpopupsenabledstate;
NPN_PopPopupsEnabledStateProcPtr poppopupsenabledstate;
NPN_EnumerateProcPtr enumerate;
NPN_PluginThreadAsyncCallProcPtr pluginthreadasynccall;
NPN_ConstructProcPtr construct;
NPN_GetValueForURLProcPtr getvalueforurl;
NPN_SetValueForURLProcPtr setvalueforurl;
NPN_GetAuthenticationInfoProcPtr getauthenticationinfo;
NPN_ScheduleTimerProcPtr scheduletimer;
NPN_UnscheduleTimerProcPtr unscheduletimer;
NPN_PopUpContextMenuProcPtr popupcontextmenu;
NPN_ConvertPointProcPtr convertpoint;
} NPNetscapeFuncs;
有超过50个函数。。。。。我没找到详细的说明文档,那就以后碰到了再研究吧,有个概念,里面提供了浏览器端的各种接口,提供给插件使用。
下面是pluginFuncs的:
typedef struct _NPPluginFuncs {
uint16 size;
uint16 version;
NPP_NewProcPtr newp;
NPP_DestroyProcPtr destroy;
NPP_SetWindowProcPtr setwindow;
NPP_NewStreamProcPtr newstream;
NPP_DestroyStreamProcPtr destroystream;
NPP_StreamAsFileProcPtr asfile;
NPP_WriteReadyProcPtr writeready;
NPP_WriteProcPtr write;
NPP_PrintProcPtr print;
NPP_HandleEventProcPtr event;
NPP_URLNotifyProcPtr urlnotify;
JRIGlobalRef javaClass;
NPP_GetValueProcPtr getvalue;
NPP_SetValueProcPtr setvalue;
} NPPluginFuncs;
这个只有13个函数,比较好看哈,我也没找到详细的说明文档。。。。但看名字大概可以猜出作用,这些都是插件提供给浏览器的回调,浏览器会在需要的时候调用它们,比如创建插件的时候调用NPP_NewProcPtr,销毁的时候调用NPP_DestroyProcPtr等。
在这里,Android为我们提供了一套接口,扩展了浏览器端的接口,定义在Android_npapi.h中:
#define kLogInterfaceV0_ANPGetValue ((NPNVariable)1000)
#define kAudioTrackInterfaceV0_ANPGetValue ((NPNVariable)1001)
#define kCanvasInterfaceV0_ANPGetValue ((NPNVariable)1002)
#define kMatrixInterfaceV0_ANPGetValue ((NPNVariable)1003)
#define kPaintInterfaceV0_ANPGetValue ((NPNVariable)1004)
#define kPathInterfaceV0_ANPGetValue ((NPNVariable)1005)
#define kTypefaceInterfaceV0_ANPGetValue ((NPNVariable)1006)
#define kWindowInterfaceV0_ANPGetValue ((NPNVariable)1007)
#define kBitmapInterfaceV0_ANPGetValue ((NPNVariable)1008)
#define kSurfaceInterfaceV0_ANPGetValue ((NPNVariable)1009)
#define kSystemInterfaceV0_ANPGetValue ((NPNVariable)1010)
#define kEventInterfaceV0_ANPGetValue ((NPNVariable)1011)
#define kSupportedDrawingModel_ANPGetValue ((NPNVariable)2000)
#define kJavaContext_ANPGetValue ((NPNVariable)2001)
上面列出的是ANPGetValue使用到的类型参数,分别可以得到相关的接口,比如Log,播放音频,绘图,控制视图,得到当前Context等等,这些接口的实现都是由WebKit引擎做的,其中有个ANPEventInterfaceV0,post事件会在NPP_HandleEventProcPtr中得到,相当于引擎提供的一个消息分发中心,这里负责分发各种系统消息,包括鼠标,键盘,触摸,绘制等,用户也可以自定义消息,具体请参考android_npapi.h,这里的消息将发送给具体的插件,后面将介绍具体的插件是怎么生成的
注意一下上面接口的NPX前缀,NPN表示浏览器端的接口(Netscape-Plugin-Netscape?),而NPP表示插件端的接口(Netscape-Plugin-Plugin?),那最开始的NP接口呢?嗯,不清楚,可能只是为了和这两种区别开来
OK,到了这里,浏览器通过MIME找到了插件,通过函数指针,告诉了插件,我浏览器提供的接口,也得到了插件提供的接口,顺便也把java环境Context传给了插件。
但初始化插件以后(NP_Initialize),WebKit将实例化插件,调用:
NPError NPP_New(NPMIMEType pluginType, NPP instance, uint16 mode, int16 argc,
char* argn[], char* argv[], NPSavedData* saved);
所以上一节提到的NP前缀,我认为可以理解成类函数,而NPP前缀相当于实例函数了,这里的NPP,就可以看成是this指针,或者C里面的叫法Handle了,看NPP的定义:
typedef struct _NPP
{
void* pdata; /* plug-in private data */
void* ndata; /* netscape private data */
} NPP_t;
typedef NPP_t* NPP;
也证实了这一点,对于它就是简单的使用它的pdata来传递参数吧
先来看看NPP_New里面的实现:
instance->pdata = browser->createobject (instance, getPluginClass());
其中browser是前面初始化的时候传过来的浏览器接口,利用它来实例化插件,getPluginClass是干嘛的?
这里的getpluginClass返回NPClass指针,其中定义了插件的一系列函数指针(又是函数指针。。。。),下面是一个实现的例子:
static NPClass pluginClass = {
NP_CLASS_STRUCT_VERSION,
pluginAllocate,
pluginDeallocate,
pluginInvalidate,
pluginHasMethod,
pluginInvoke,
pluginInvokeDefault,
pluginHasProperty,
pluginGetProperty,
pluginSetProperty,
pluginRemoveProperty,
pluginEnumerate
};
这些函数指针应该是预先定义好的,顺序,个数都要保证,由于以前开发过相关的这部分内容,我知道这些是浏览器用来操作NPObject的接口,任意一个js对象,调用它的属性或者方法,最终都是调用到对应的NPObject中来的,这样来看,这个NPObject代表了对应的一个js的对象,我们这里就是对应了MIME那个节点。
浏览器知道了这个NPObject的接口,就直接利用pluginAllocate建立这个NPObject,回调。。。又是回调,插件的地位很低的,告诉别人怎么样调用自己,但啥时候被调不是自己说了算的
实际中,我们创建了一个PluginObject,这个相当于是插件这边的Handle,里面保存了很多东东:
typedef struct PluginObject {
NPObject header;
NPP npp;
NPWindow* window;
PluginType pluginType;
SubPlugin* activePlugin;
} PluginObject;
static NPObject *pluginAllocate(NPP npp, NPClass *theClass)
{
PluginObject *newInstance = (PluginObject*) malloc(sizeof(PluginObject));
newInstance->header._class = theClass;
newInstance->header.referenceCount = 1;
if (!identifiersInitialized) {
identifiersInitialized = true;
initializeIdentifiers();
}
newInstance->npp = npp;
return &newInstance->header;
}
这里可以看到,NPObject就是NPClass的封装,代表了js的对象本质就是一组函数
NPWindow在哪里赋值的?还记得NPP_SetWindow吧,这个应该是在插件视图创建好以后调用的。
嗯,还没有完,继续NPP_New,看看这3个参数
int16 argc,char* argn[], char* argv[]
类似main函数的参数,argn和argv分别代表节点的属性名和属性值,我对HTML不是很熟,不知道是不是叫这个,还是对照测试用的HTML文件来解释好一些:
<object type="application/x-testbrowserplugin" height=200 width=400 id="sample">
<param name="DrawingModel" value="Surface" />
<param name="PluginType" value="Paint" />
</object>
OK,这里的argc == 2,argn[0] == "DrawingModel",argn[1] == "Surface",这样大家应该就明白了
浏览器通过HTML中插件的参数,调用NPP_New初始化插件,得到SubPlugin和PluginType
这里的SubPlugin代表真正的插件实例的接口,定义如下:
class SubPlugin {
public:
SubPlugin(NPP inst) : m_inst(inst) {}
virtual ~SubPlugin() {}
virtual int16 handleEvent(const ANPEvent* evt) = 0;
virtual bool supportsDrawingModel(ANPDrawingModel) = 0;
int getPluginWidth();
int getPluginHeight();
NPP inst() const { return m_inst; }
private:
NPP m_inst;
};
class SurfaceSubPlugin : public SubPlugin {
public:
SurfaceSubPlugin(NPP inst) : SubPlugin(inst) { m_context = NULL; }
virtual ~SurfaceSubPlugin() {}
virtual jobject getSurface() = 0;
virtual bool supportsDrawingModel(ANPDrawingModel);
void setContext(jobject context);
jobject m_context;
};
SubPlugin,提供处理事件接口,SurfaceSubPlugin继承SubPlugin,增加了Context,用作jni相关的调用
实际中,我们继承SurfaceSubPlugin,实现这些纯虚函数,创建我们自己的插件,而HandleEvent的事件就是前面讲到了的在NPP_HandleEvent中传入的
OK,到现在为止,初始化的工作做完了,基本的调用逻辑,框架也搭好了,可以开始具体的插件实现了。