extension_extensions - CSDN
精华内容
参与话题
  • 摘要:本文主要描述关于chrome plugin开发的相关开发知识,着重讲述下popup/background/content-script三者之间的消息互通。 一、前言 在此之前,并未接触到chrome插件开发,由于最近业务需要,不得不去了解一些...
     

    摘要:本文主要描述关于chrome plugin开发的相关开发知识,着重讲述下popup/background/content-script三者之间的消息互通。

    一、前言

    在此之前,并未接触到chrome插件开发,由于最近业务需要,不得不去了解一些相关内容。在实践的过程中,总结了一些内容,一来方便自己日后查阅,二来希望给众位同僚提供一些参考。如有不对之处,还望及时指正。

    一、Chrome Extension还是Chrome Plugin?

    事实上,本文所说的插件并不是严格意义上的Chrome Plugin,从真正意义上讲,Chrome Plugin是浏览器底层的功能实现,是需要有一定的浏览器开发能力才可以接触到的层面。而我们习惯上更喜欢叫Chrome插件,但作为一个正直的developer,我们需要知道这二者是不同的东西。所以,在本文中出现的chrome extension扩展和chrome plugin插件都是指同一个东西。

    二、为什么选择Chrome浏览器?

    众所周知,Chrome浏览器在全球都拥有可观的忠实用户,抛去其占据了浏览器市场的霸主地位不说,其具备了众多的优点,如良好的用户体验,简单的开发规范等等。

    归纳为以下几点:

    1. 市场占有率高,用户基础庞大;
    2. 开发流程简单,方便快速上手;
    3. 应用场景广泛,兼容webkit内核360极速浏览器、360安全浏览器搜狗浏览器、QQ浏览器等等);
    4. 可扩展性强,非weikit内核的浏览器也有一定的支持(如Firefox)

    注:

    打开chrome浏览器地址栏是输入chrome://extensions/可以打开chrome插件管理;

    打开360游览器地址栏里输入 se://extensions-frame/ 可以打开360插件管理;

    三、关于Chrome Extension

    Chrome Extension简单定义为浏览器的功能性扩展,由html、css、js和一个描述文件manifest.json组成,在浏览器的地址栏边上显示扩展图标。本质上其实就是一个由html、css、js、图片等资源组成的一个.crx后缀的压缩包(crx:ChRome eXtension)

    四、开发调试

    1. 调试弹出页(popup)

    右击扩展图标->审查弹出内容即可弹出开发者面板,这个面板与网页调试面板一模一样,操作方式也是相同的。值得一提的是第一次弹出面板,会错过弹出页,初始化的脚本,可以通过在对应的面板上按F5然它重新加载进入断点

    2. 调试选项页(option)

    右击扩展图标->选项,在选项页按F12打开调试面板

    3. 调试后台页(background)

    点击检查视图后的超链接,就会弹出后台页相关的调试面板。如图所示:

    4. 调试内容脚本(content script)

    在内容脚本注入的网页打开开发者面板->source->Content scripts(左侧面板)

    五、Manifest 文件

    每一个扩展、可安装的WebApp皮肤,都有一个JSON格式的manifest文件用来配置所有和扩展相关的配置,而且必须放在扩展的根目录。其中,名称(name)、版本(version)和Manifest 版本(manifest_version)这3个是必须添加的(而且manifest_version 必须为 2),描述(description)、图标位置(icons)是推荐的。

    归纳一下manifest文件常见的配置项

    {
    	//(必须)manifest版本,而且必须是2
    	"manifest_version": 2,
    	// (必须)名称
    	"name": "demo",
    	// (必须)版本
    	"version": "1.0.0",
    	// (推荐)描述
    	"description": "简单的Chrome扩展demo",
    	// (推荐)图标,懒加载可用一个尺寸
    	"icons":
    	{
    	    "16": "images/icon-16.png",
    	    "32": "images/icon-32.png",
    	    "48": "images/icon-48.png",
    	    "64": "images/icon-64.png",
    	    "128": "images/icon-128.png"
    	},
    	// background script即插件运行的环境,会一直常驻的后台JS或后台页面
    	"background":
    	{
    		// 2种指定方式,如果指定JS,那么会自动生成一个背景页
    		"page": "background.html"
    		//"scripts": ["js/background.js"]
    	},
    	// 浏览器右上角图标设置,browser_action、page_action、app必须三选一
    	//	注意: Packaged apps 不能使用browser actions.
    	"browser_action": 
    	{
    		"default_icon": "images/icon.png",
    		"default_title": "hello", // 图标悬停时的标题,可选
    		"default_popup": "popup.html"
    	},
    	// 当某些特定页面打开才显示的图标
    	/*"page_action":
    	{
    		"default_icon": "images/icon.png",
    		"default_title": "hello",
    		"default_popup": "popup.html"
    	},*/
    	// 需要直接注入页面的JS(使插件可以访问页面上的资源)
    	"content_scripts": 
    	[
    		{
    			//"matches": ["http://*/*", "https://*/*"],
    			// "<all_urls>" 表示匹配所有地址
    			"matches": ["<all_urls>"],
    			// 多个JS按顺序注入
    			"js": ["js/jquery-1.8.3.js", "js/content-script.js"],
    			// JS的注入可以随便一点,但是CSS的注意就要千万小心了,因为一不小心就可能影响全局样式
    			"css": ["css/custom.css"],
    			// 代码注入的时间,可选值: "document_start", "document_end", or "document_idle",最后一个表示页面空闲时,默认document_idle
    			"run_at": "document_start"
    		},
    		// 这里仅仅是为了演示content-script可以配置多个规则
    		{
    			"matches": ["*://*/*.png", "*://*/*.jpg", "*://*/*.gif", "*://*/*.bmp"],
    			"js": ["js/show-image-content-size.js"]
    		}
    	],
    	// 权限申请
    	"permissions":
    	[
    		"contextMenus", // 右键菜单
    		"tabs", // 标签
    		"notifications", // 通知
    		"webRequest", // web请求
    		"webRequestBlocking",
    		"storage", // 插件本地存储
    		"http://*/*", // 可以通过executeScript或者insertCSS访问的网站
    		"https://*/*" // 可以通过executeScript或者insertCSS访问的网站
    	],
    	// 使普通页面能够直接访问插件资源,如脚本代码等,如果不设置是无法直接访问的(这些代码直接嵌入到页面中)
    	"web_accessible_resources": ["js/inject.js"],
    	// 扩展的主页 url。扩展的管理界面里面将有一个链接指向这个url。如果你将扩展放在自己的网站上,这个url就很有用了。如果你通过了Extensions Gallery和Chrome Web Store来分发扩展,主页 缺省就是扩展的页面。
    	"homepage_url": "https://www.baidu.com",
    	// 覆盖浏览器默认页面
    	"chrome_url_overrides":
    	{
    		// 覆盖浏览器默认的新标签页
    		"newtab": "newtab.html"
    	},
    	// Chrome40以前的插件选项页写法
    	"options_page": "options.html",
    	// Chrome40以后的插件选项页写法,如果2个都写,新版Chrome只认后面这一个
    	"options_ui":
    	{
    		"page": "options.html",
    		// 添加一些默认的样式,推荐使用
    		"chrome_style": true
    	},
    	// 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字
    	"omnibox": { "keyword" : "go" },
    	// 默认语言
    	"default_locale": "zh_CN",
    	// devtools页面入口,注意只能指向一个HTML文件,不能是JS文件
    	"devtools_page": "devtools.html"
    }
    

    从Chrome 18版本开始,Manifest V1就开始进入了淘汰的过程。Chrome内核对 Manifest V1 的支持计划具体可参见 Google 开发者网站上的日程表:manifest version 1 support schedule

    六、content-script文件(页面内容脚本)

    如果一个应用(扩展)需要与web页面交互,那么就需要使用一个content script。Content script脚本是指能够在浏览器已经加载的页面内部运行的javascript脚本。可以将content script看作是网页的一部分,而不是它所在的应用(扩展)的一部分。

    Content script可以获得浏览器所访问的web页面的详细信息,并可以对页面做出修改。

    Content script与它所在的应用(扩展)并不是完全没有联系。一个content script脚本可以与所在的应用(扩展)交换消息

     

    下面是content scipt可以做的一些事情范例:

    • 从页面中找到没有写成超链接形式的url,并将它们转成超链接。
    • 放大页面字体使文字更清晰
    • 找到并处理DOM中的microformat

    当然,content scripts也有一些限制,它们不能做的事情包括 :

    • 不能使用除了chrome.extension之外的chrome.* 的接口
    • 不能访问它所在扩展中定义的函数和变量
    • 不能访问web页面或其它content script中定义的函数和变量
    • 不能做cross-site XMLHttpRequests

    这些限制其实并不像看上去那么糟糕。Content scripts 可以使用messages机制与它所在的扩展通信,来间接使用chrome.*接口,或访问扩展数据。Content scripts还可以通过共享的DOM来与web页面通信

    content-script文件是嵌入到匹配的网页中的脚本,但是又与页面中的脚本隔离开。虽然可以操纵页面上的DOM元素,但却不能够使用页面脚本的API。也就是运行环境与页面的脚本是隔离开的

    content-script和原始页面共享DOM,但是不共享JS如要访问页面JS(例如某个JS变量),只能通过injected js来实现content-scripts不能访问绝大部分chrome.xxx.api,除了下面这4种

    chrome.extension(getURL , inIncognitoContext , lastError , onRequest , sendRequest)
    chrome.i18n
    chrome.runtime(connect , getManifest , getURL , id , onConnect , onMessage , sendMessage)
    chrome.storage

    重要!!!除了可以在manifest中配置需要注入的页面以外,还可以动态的注入到页面中。

        //直接注入代码
        chrome.browserAction.onClicked.addListener(function(tab) {
          chrome.tabs.executeScript({
            code: 'document.body.style.backgroundColor="red"'
          });
        });
        //注入脚本文件
        chrome.tabs.executeScript(null, {file: "content_script.js"});
    
    

    要在manifest文件中配置权限

        "permissions": [
          "activeTab"
        ],
    

    七、background文件

    background 可以使扩展常驻后台,比较常用的是指定子属性scripts,表示在扩展启动时自动创建一个包含所有指定脚本的页面。它的生命周期是插件中所有类型页面中最长的,它随着浏览器的打开而打开,随着浏览器的关闭而关闭,所以通常把需要一直运行的、启动就运行的、全局的代码放在background里面

    background的权限非常高,几乎可以调用所有的Chrome扩展API(除了devtools),而且它可以无限制跨域,也就是可以跨域访问任何网站而无需要求对方设置CORS

    经过测试,其实不止是background,所有的直接通过chrome-extension://id/xx.html这种方式打开的网页都可以无限制跨域。

    八、popup文件(弹窗)

    如果browser action拥有一个popup,popup 会在用户点击图标后出现。popup 可以包含任意你想要的HTML内容,并且会自适应大小。所以一般用来做临时性的交互

    在你的browser action中添加一个popup,创建弹出的内容的HTML文件。 修改browser_action的manifest中default_popup字段来指定HTML文件, 或者调用setPopup()方法。

    在权限上,它和background非常类似,它们之间最大的不同是生命周期的不同,popup中可以直接通过chrome.extension.getBackgroundPage()获取background的window对象。

    九、消息通信机制

    废话不多说,都在代码里了。源代码

    消息交互图

    -_-,图略丑陋,将就看一下,手动微笑。

    1. popup.html文件

    <!doctype html>
    <html lang="zh">
    <head>
    	<meta charset="UTF-8">
    </head>
    <body style="width: 300px">	
        <div width="200px">
          <button style="margin:5px 5px 5px 5px" id="con">popup发送消息给content_scripts</button>
          <button style="margin:5px 5px 5px 5px" id="bg">popup调用background的js函数</button>
          <button style="margin:5px 5px 5px 5px" id="bgtocon">background发送消息给content_scripts</button>
        </div>
    </body>
    </html>
    
    <script type="text/javascript" src="js/jquery.js"></script>
    <script type="text/javascript" src="js/popup.js"></script>
    

    2. popup.js文件

    
    // popup调用background的js函数
    $('#bg').click(() => {
    	//alert("调用background的js函数");
    	var bg = chrome.extension.getBackgroundPage();
    	console.log(123123, bg)
    	bg.bgtest();
    });
    
     // popup主动发消息给content-script
    $('#con').click(() => {
    	alert("popup发送消息给content-script");
    	sendMessageToContentScript('你好,我是popup!', (response) => {
    		if(response) alert('收到来自content-script的回复:'+response);
    	});
    });
    
    // popup调用background的js函数
    $('#bgtocon').click(() => {
    	var bg = chrome.extension.getBackgroundPage();
    	bg.TT();
    });
    
    // 获取当前选项卡ID
    function getCurrentTabId(callback)
    {
    	chrome.tabs.query({active: true, currentWindow: true}, function(tabs)
    	{
    		if(callback) callback(tabs.length ? tabs[0].id: null);
    	});
    }
    
    // 向content-script主动发送消息
    function sendMessageToContentScript(message, callback)
    {
    	getCurrentTabId((tabId) =>
    	{
    		chrome.tabs.sendMessage(tabId, message, function(response)
    		{
    			if(callback) callback(response);
    		});
    	});
    }
    

    3. background.js文件

    function bgtest()
    {
    	alert("background的bgtest函数!");
    }
    
    // 监听来自content-script的消息
    chrome.runtime.onMessage.addListener(function(request, sender, sendResponse)
    {
    	console.log('收到来自content-script的消息:');
    	console.log(request, sender, sendResponse);
    	sendResponse('我是background,我已收到你的消息:' + JSON.stringify(request));
    });
    
    // backgrond向context_scripts发送消息
    function TT(){
    	
    	sendMessageToContentScript('context_scripts你好,我是backgrond!', (response) => {
    	if(response) alert('backgrond收到来自content-script的回复:'+response);
    	});
    }
    
    // 获取当前选项卡ID
    function getCurrentTabId(callback)
    {
    	chrome.tabs.query({active: true, currentWindow: true}, function(tabs)
    	{
    		if(callback) callback(tabs.length ? tabs[0].id: null);
    	});
    }
    
    // 向content-script主动发送消息
    function sendMessageToContentScript(message, callback)
    {
    	getCurrentTabId((tabId) =>
    	{
    		chrome.tabs.sendMessage(tabId, message, function(response)
    		{
    			if(callback) callback(response);
    		});
    	});
    }
    

    4. content-script文件

    //常驻后台,并且会注入到页面中
    alert("content-script.js 已经注入");
    
    //直接调用注入的其他的js函数 注入的js可以有多个在mainfest中配置
    //aa();
    
    // 接收来自后台的消息
    chrome.runtime.onMessage.addListener(function(request, sender, sendResponse)
    {
    	console.log('收到来自 ' + (sender.tab ? "content-script(" + sender.tab.url + ")" : "popup或者background") + ' 的消息:', request);
    	tip(JSON.stringify(request));
    	sendResponse('我收到你的消息了:'+JSON.stringify(request));
    
    });
    
    
     //与后端background进行消息交互	 执行6秒
    var startTime = new Date().getTime(); 
    var interval = setInterval(function ()
    { 
    	if(new Date().getTime() - startTime > 6000)
    	{
    		clearInterval(interval);
    		return;
    	}
            chrome.runtime.sendMessage
            (
    	      {
    		    doc: "yes",
    		    data:"123",
    	      },
    	      function(response) 
    	      {
    		
       		    tip(JSON.stringify("content-script向background 发送消息"));	
       		    tip('收到来自background的回复:' + response);
    	      }
            );
    },500);
    
    
    
    
    var tipCount = 0;
    // 简单的消息通知
    function tip(info) {
    	info = info || '';
    	var ele = document.createElement('div');
    	ele.className = 'chrome-plugin-simple-tip slideInLeft';
    	ele.style.top = tipCount * 70 + 20 + 'px';
    	ele.innerHTML = `<div>${info}</div>`;
    	document.body.appendChild(ele);
    	ele.classList.add('animated');
    	tipCount++;
    	setTimeout(() => {
    		ele.style.top = '-100px';
    		setTimeout(() => {
    			ele.remove();
    			tipCount--;
    		}, 4000);
    	}, 5000);
    }
    
    

     

    十、参考资料

    360安全浏览器开发文档(推荐)
    360极速浏览器Chrome扩展开发文档
    chrome扩展开发官方文档
    chrome API支持
    【干货】Chrome插件(扩展)开发全攻略(很详细,文中部分内容来源于此)
    Chrome扩展开发概述

    展开全文
  • 扩展点加载机制(ExtensionLoader)

    万次阅读 2015-04-08 21:52:50
    概述来源: Dubbo的扩展点加载从JDK标准的SPI(Service Provider Interface)扩展点发现机制加强而来。Dubbo改进了JDK标准的SPI的以下问题: + JDK标准的SPI会一次性实例化扩展点所有实现,如果有扩展实现初始化很...

    概述

    来源:
    Dubbo的扩展点加载从JDK标准的SPI(Service Provider Interface)扩展点发现机制加强而来。

    Dubbo改进了JDK标准的SPI的以下问题:

    • JDK标准的SPI会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。

    • 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK标准的ScriptEngine,通过getName();获取脚本类型的名称,但如果RubyScriptEngine因为所依赖的jruby.jar不存在,导致RubyScriptEngine类加载失败,这个失败原因被吃掉了,和ruby对应不起来,当用户执行ruby脚本时,会报不支持ruby,而不是真正失败的原因。

    • 增加了对扩展点IoC和AOP的支持,一个扩展点可以直接setter注入其它扩展点。

    约定:
    在扩展类的jar包内,放置扩展点配置文件:META-INF/dubbo/接口全限定名,内容为:配置名=扩展实现类全限定名,多个实现类用换行符分隔。
    (注意:这里的配置文件是放在你自己的jar包内,不是dubbo本身的jar包内,Dubbo会全ClassPath扫描所有jar包内同名的这个文件,然后进行合并)

    扩展Dubbo的协议示例:
    在协议的实现jar包内放置文本文件:META-INF/dubbo/com.alibaba.dubbo.rpc.Protocol,内容为:

    xxx=com.alibaba.xxx.XxxProtocol

    实现内容:

    package com.alibaba.xxx;
    
    import com.alibaba.dubbo.rpc.Protocol;
    public class XxxProtocol implemenets Protocol {
    
        // ...
    }

    注意: 扩展点使用单一实例加载(请确保扩展实现的线程安全性),Cache在ExtensionLoader中

    特性

    • 扩展点自动包装
    • 扩展点自动装配
    • 扩展点自适应
    • 扩展点自动激活

    相关文档可以参考dubbo的官方文档 ,本文主要通过分析相关的源代码来体会dubbo的扩展点框架提供的特性。


    源码分析

    dubbo的扩展点框架主要位于这个包下:

    com.alibaba.dubbo.common.extension

    大概结构如下:

    com.alibaba.dubbo.common.extension
     |
     |--factory
     |     |--AdaptiveExtensionFactory   #稍后解释
     |     |--SpiExtensionFactory        #稍后解释
     |
     |--support
     |     |--ActivateComparator
     |
     |--Activate  #自动激活加载扩展的注解
     |--Adaptive  #自适应扩展点的注解
     |--ExtensionFactory  #扩展点对象生成工厂接口
     |--ExtensionLoader   #扩展点加载器,扩展点的查找,校验,加载等核心逻辑的实现类
     |--SPI   #扩展点注解

    其中最核心的类就是ExtensionLoader,几乎所有特性都在这个类中实现,先来看下他的结构:

    ExtensionLoader

    ExtensionLoader没有提供public的构造方法,但是提供了一个public staticgetExtensionLoader,这个方法就是获取ExtensionLoader实例的工厂方法。其public成员方法中有三个比较重要的方法:

    • getActivateExtension :根据条件获取当前扩展可自动激活的实现
    • getExtension : 根据名称获取当前扩展的指定实现
    • getAdaptiveExtension : 获取当前扩展的自适应实现

    这三个方法将会是我们重点关注的方法;* 每一个ExtensionLoader实例仅负责加载特定SPI扩展的实现*。因此想要获取某个扩展的实现,首先要获取到该扩展对应的ExtensionLoader实例,下面我们就来看一下获取ExtensionLoader实例的工厂方法getExtensionLoader

    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        if (type == null)
            throw new IllegalArgumentException("Extension type == null");
        if(!type.isInterface()) {
            throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
        }
        if(!withExtensionAnnotation(type)) { // 只接受使用@SPI注解注释的接口类型
            throw new IllegalArgumentException("Extension type(" + type + 
                    ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
        }
    
        // 先从静态缓存中获取对应的ExtensionLoader实例
        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        if (loader == null) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type)); // 为Extension类型创建ExtensionLoader实例,并放入静态缓存
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }

    该方法需要一个Class类型的参数,该参数表示希望加载的扩展点类型,该参数必须是接口,且该接口必须被@SPI注解注释,否则拒绝处理。检查通过之后首先会检查ExtensionLoader缓存中是否已经存在该扩展对应的ExtensionLoader,如果有则直接返回,否则创建一个新的ExtensionLoader负责加载该扩展实现,同时将其缓存起来。可以看到对于每一个扩展,dubbo中只会有一个对应的ExtensionLoader实例。

    接下来看下ExtensionLoader的私有构造函数:

    private ExtensionLoader(Class<?> type) {
        this.type = type;
    
        // 如果扩展类型是ExtensionFactory,那么则设置为null
        // 这里通过getAdaptiveExtension方法获取一个运行时自适应的扩展类型(每个Extension只能有一个@Adaptive类型的实现,如果没有dubbo会动态生成一个类)
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }

    这里保存了对应的扩展类型,并且设置了一个额外的objectFactory属性,他是一个ExtensionFactory类型,ExtensionFactory主要用于加载扩展的实现:

    @SPI
    public interface ExtensionFactory {
    
        /**
         * Get extension.
         * 
         * @param type object type.
         * @param name object name.
         * @return object instance.
         */
        <T> T getExtension(Class<T> type, String name);
    
    }

    同时ExtensionFactory也被@SPI注解注释,说明他也是一个扩展点,从前面com.alibaba.dubbo.common.extension包的结构图中可以看到,dubbo内部提供了两个实现类:SpiExtensionFactoryAdaptiveExtensionFactory,实际上还有一个SpringExtensionFactory,不同的实现可以已不同的方式来完成扩展点实现的加载,这块稍后再来学习。从ExtensionLoader的构造函数中可以看到,如果要加载的扩展点类型是ExtensionFactory是,object字段被设置为null。由于ExtensionLoader的使用范围有限(基本上局限在ExtensionLoader中),因此对他做了特殊对待:在需要使用ExtensionFactory的地方,都是通过对应的自适应实现来代替。

    默认的ExtensionFactory实现中,AdaptiveExtensionFactotry@Adaptive注解注释,也就是它就是ExtensionFactory对应的自适应扩展实现(每个扩展点最多只能有一个自适应实现,如果所有实现中没有被@Adaptive注释的,那么dubbo会动态生成一个自适应实现类),也就是说,所有对ExtensionFactory调用的地方,实际上调用的都是AdpativeExtensionFactory,那么我们看下他的实现代码:

    @Adaptive
    public class AdaptiveExtensionFactory implements ExtensionFactory {
    
        private final List<ExtensionFactory> factories;
    
        public AdaptiveExtensionFactory() {
            ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
            List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
            for (String name : loader.getSupportedExtensions()) { // 将所有ExtensionFactory实现保存起来
                list.add(loader.getExtension(name));
            }
            factories = Collections.unmodifiableList(list);
        }
    
        public <T> T getExtension(Class<T> type, String name) {
            // 依次遍历各个ExtensionFactory实现的getExtension方法,一旦获取到Extension即返回
            // 如果遍历完所有的ExtensionFactory实现均无法找到Extension,则返回null
            for (ExtensionFactory factory : factories) {
                T extension = factory.getExtension(type, name);
                if (extension != null) {
                    return extension;
                }
            }
            return null;
        }
    
    }

    看完代码大家都知道是怎么回事了,这货就相当于一个代理入口,他会遍历当前系统中所有的ExtensionFactory实现来获取指定的扩展实现,获取到扩展实现或遍历完所有的ExtensionFactory实现。这里调用了ExtensionLoadergetSupportedExtensions方法来获取ExtensionFactory的所有实现,又回到了ExtensionLoader类,下面我们就来分析ExtensionLoader的几个重要的实例方法。

    方法调用流程

    getExtension

    getExtension(name)
        -> createExtension(name) #如果无缓存则创建
            -> getExtensionClasses().get(name) #获取name对应的扩展类型
            -> 实例化扩展类
            -> injectExtension(instance) # 扩展点注入
            -> instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)) #循环遍历所有wrapper实现,实例化wrapper并进行扩展点注入  

    getAdaptiveExtension

    public T getAdaptiveExtension()
        -> createAdaptiveExtension() #如果无缓存则创建
            -> getAdaptiveExtensionClass().newInstance() #获取AdaptiveExtensionClass
                -> getExtensionClasses() # 加载当前扩展所有实现,看是否有实现被标注为@Adaptive
                -> createAdaptiveExtensionClass() #如果没有实现被标注为@Adaptive,则动态创建一个Adaptive实现类
                    -> createAdaptiveExtensionClassCode() #动态生成实现类java代码
                    -> compiler.compile(code, classLoader) #动态编译java代码,加载类并实例化
            -> injectExtension(instance)

    getActivateExtesion
    该方法有多个重载方法,不过最终都是调用了三个参数的那一个重载形式。其代码结构也相对剪短,就不需要在列出概要流程了。


    详细代码分析

    getAdaptiveExtension
    从前面ExtensionLoader的私有构造函数中可以看出,在选择ExtensionFactory的时候,并不是调用getExtension(name)来获取某个具体的实现类,而是调用getAdaptiveExtension来获取一个自适应的实现。那么首先我们就来分析一下getAdaptiveExtension这个方法的实现吧:

    public T getAdaptiveExtension() {
        Object instance = cachedAdaptiveInstance.get(); // 首先判断是否已经有缓存的实例对象
        if (instance == null) {
            if(createAdaptiveInstanceError == null) {
                synchronized (cachedAdaptiveInstance) {
                    instance = cachedAdaptiveInstance.get();
                    if (instance == null) {
                        try {
                            instance = createAdaptiveExtension(); // 没有缓存的实例,创建新的AdaptiveExtension实例
                            cachedAdaptiveInstance.set(instance);
                        } catch (Throwable t) {
                            createAdaptiveInstanceError = t;
                            throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t);
                        }
                    }
                }
            }
            else {
                throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
            }
        }
    
        return (T) instance;
    }

    首先检查缓存的adaptiveInstance是否存在,如果存在则直接使用,否则的话调用createAdaptiveExtension方法来创建新的adaptiveInstance并且缓存起来。也就是说对于某个扩展点,每次调用ExtensionLoader.getAdaptiveExtension获取到的都是同一个实例。

    private T createAdaptiveExtension() {
        try {
            return injectExtension((T) getAdaptiveExtensionClass().newInstance()); // 先获取AdaptiveExtensionClass,在获取其实例,最后进行注入处理
        } catch (Exception e) {
            throw new IllegalStateException("Can not create adaptive extenstion " + type + ", cause: " + e.getMessage(), e);
        }
    }

    createAdaptiveExtension方法中,首先通过getAdaptiveExtensionClass方法获取到最终的自适应实现类型,然后实例化一个自适应扩展实现的实例,最后进行扩展点注入操作。先看一个getAdaptiveExtensionClass方法的实现:

    private Class<?> getAdaptiveExtensionClass() {
        getExtensionClasses(); // 加载当前Extension的所有实现,如果有@Adaptive类型,则会赋值为cachedAdaptiveClass属性缓存起来
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        return cachedAdaptiveClass = createAdaptiveExtensionClass(); // 没有找到@Adaptive类型实现,则动态创建一个AdaptiveExtensionClass
    }

    他只是简单的调用了getExtensionClasses方法,然后在判adaptiveCalss缓存是否被设置,如果被设置那么直接返回,否则调用createAdaptiveExntesionClass方法动态生成一个自适应实现,关于动态生成自适应实现类然后编译加载并且实例化的过程这里暂时不分析,留到后面在分析吧。这里我们看getExtensionClassses方法:

    private Map<String, Class<?>> getExtensionClasses() {
        Map<String, Class<?>> classes = cachedClasses.get(); // 判断是否已经加载了当前Extension的所有实现类
        if (classes == null) {
            synchronized (cachedClasses) {
                classes = cachedClasses.get();
                if (classes == null) {
                    classes = loadExtensionClasses(); // 如果还没有加载Extension的实现,则进行扫描加载,完成后赋值给cachedClasses变量
                    cachedClasses.set(classes);
                }
            }
        }
        return classes;
    }

    getExtensionClasses方法中,首先检查缓存的cachedClasses,如果没有再调用loadExtensionClasses方法来加载,加载完成之后就会进行缓存。也就是说对于每个扩展点,其实现的加载只会执行一次。我们看下loadExtensionClasses方法:

    private Map<String, Class<?>> loadExtensionClasses() {
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        if(defaultAnnotation != null) {
            String value = defaultAnnotation.value(); // 解析当前Extension配置的默认实现名,赋值给cachedDefaultName属性
            if(value != null && (value = value.trim()).length() > 0) {
                String[] names = NAME_SEPARATOR.split(value);
                if(names.length > 1) { // 每个扩展实现只能配置一个名称
                    throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
                            + ": " + Arrays.toString(names));
                }
                if(names.length == 1) cachedDefaultName = names[0];
            }
        }
    
        // 从配置文件中加载扩展实现类
        Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
        loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
        loadFile(extensionClasses, DUBBO_DIRECTORY);
        loadFile(extensionClasses, SERVICES_DIRECTORY);
        return extensionClasses;
    }

    从代码里面可以看到,在loadExtensionClasses中首先会检测扩展点在@SPI注解中配置的默认扩展实现的名称,并将其赋值给cachedDefaultName属性进行缓存,后面想要获取该扩展点的默认实现名称就可以直接通过访问cachedDefaultName字段来完成,比如getDefaultExtensionName方法就是这么实现的。从这里的代码中又可以看到,具体的扩展实现类型,是通过调用loadFile方法来加载,分别从一下三个地方加载:

    • META-INF/dubbo/internal/
    • META-INF/dubbo/
    • META-INF/services/

    那么这个loadFile方法则至关重要了,看看其源代码:

    private void loadFile(Map<String, Class<?>> extensionClasses, String dir) {
        String fileName = dir + type.getName(); // 配置文件名称,扫描整个classpath
        try {
            // 先获取该路径下所有文件
            Enumeration<java.net.URL> urls;
            ClassLoader classLoader = findClassLoader();
            if (classLoader != null) {
                urls = classLoader.getResources(fileName);
            } else {
                urls = ClassLoader.getSystemResources(fileName);
            }
            if (urls != null) {
                // 遍历这些文件并进行处理
                while (urls.hasMoreElements()) {
                    java.net.URL url = urls.nextElement(); // 获取配置文件路径
                    try {
                        BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), "utf-8"));
                        try {
                            String line = null;
                            while ((line = reader.readLine()) != null) { // 一行一行读取(一行一个配置)
                                final int ci = line.indexOf('#');
                                if (ci >= 0) line = line.substring(0, ci);
                                line = line.trim();
                                if (line.length() > 0) {
                                    try {
                                        String name = null;
                                        int i = line.indexOf('='); // 等号分割
                                        if (i > 0) {
                                            name = line.substring(0, i).trim(); // 扩展名称
                                            line = line.substring(i + 1).trim(); // 扩展实现类
                                        }
                                        if (line.length() > 0) {
                                            Class<?> clazz = Class.forName(line, true, classLoader); // 加载扩展实现类
                                            if (! type.isAssignableFrom(clazz)) { // 判断类型是否匹配
                                                throw new IllegalStateException("Error when load extension class(interface: " +
                                                        type + ", class line: " + clazz.getName() + "), class " 
                                                        + clazz.getName() + "is not subtype of interface.");
                                            }
                                            if (clazz.isAnnotationPresent(Adaptive.class)) { // 判断该实现类是否@Adaptive,是的话不会放入extensionClasses/cachedClasses缓存
                                                if(cachedAdaptiveClass == null) { // 第一个赋值给cachedAdaptiveClass属性
                                                    cachedAdaptiveClass = clazz;
                                                } else if (! cachedAdaptiveClass.equals(clazz)) { // 只能有一个@Adaptive实现,出现第二个就报错了
                                                    throw new IllegalStateException("More than 1 adaptive class found: "
                                                            + cachedAdaptiveClass.getClass().getName()
                                                            + ", " + clazz.getClass().getName());
                                                }
                                            } else { // 不是@Adaptive类型
                                                try {
                                                    clazz.getConstructor(type); // 判断是否Wrapper类型
                                                    Set<Class<?>> wrappers = cachedWrapperClasses;
                                                    if (wrappers == null) {
                                                        cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
                                                        wrappers = cachedWrapperClasses;
                                                    }
                                                    wrappers.add(clazz); //放入到Wrapper实现类缓存中
                                                } catch (NoSuchMethodException e) { //不是Wrapper类型,普通实现类型
                                                    clazz.getConstructor();
                                                    if (name == null || name.length() == 0) {
                                                        name = findAnnotationName(clazz);
                                                        if (name == null || name.length() == 0) {
                                                            if (clazz.getSimpleName().length() > type.getSimpleName().length()
                                                                    && clazz.getSimpleName().endsWith(type.getSimpleName())) {
                                                                name = clazz.getSimpleName().substring(0, clazz.getSimpleName().length() - type.getSimpleName().length()).toLowerCase();
                                                            } else {
                                                                throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + url);
                                                            }
                                                        }
                                                    }
                                                    String[] names = NAME_SEPARATOR.split(name); // 看是否配置了多个name
                                                    if (names != null && names.length > 0) {
                                                        Activate activate = clazz.getAnnotation(Activate.class); // 是否@Activate类型
                                                        if (activate != null) {
                                                            cachedActivates.put(names[0], activate);// 是则放入cachedActivates缓存
                                                        }
    
                                                        // 遍历所有name
                                                        for (String n : names) {
                                                            if (! cachedNames.containsKey(clazz)) {
                                                                cachedNames.put(clazz, n); // 放入Extension实现类与名称映射缓存,每个class只对应第一个名称有效
                                                            }
                                                            Class<?> c = extensionClasses.get(n);
                                                            if (c == null) {
                                                                extensionClasses.put(n, clazz); // 放入到extensionClasses缓存,多个name可能对应一个Class
                                                            } else if (c != clazz) { // 存在重名
                                                                throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    } catch (Throwable t) {
                                        IllegalStateException e = new IllegalStateException("Failed to load extension class(interface: " + type + ", class line: " + line + ") in " + url + ", cause: " + t.getMessage(), t);
                                        exceptions.put(line, e);
                                    }
                                }
                            } // end of while read lines
                        } finally {
                            reader.close();
                        }
                    } catch (Throwable t) {
                        logger.error("Exception when load extension class(interface: " +
                                            type + ", class file: " + url + ") in " + url, t);
                    }
                } // end of while urls
            }
        } catch (Throwable t) {
            logger.error("Exception when load extension class(interface: " +
                    type + ", description file: " + fileName + ").", t);
        }
    }

    代码比较长,大概的事情呢就是解析配置文件,获取扩展点实现对应的名称和实现类,并进行分类处理和缓存。当loadFile方法执行完成之后,以下几个变量就会被附上值:

    • cachedAdaptiveClass : 当前Extension类型对应的AdaptiveExtension类型(只能一个)
    • cachedWrapperClasses : 当前Extension类型对应的所有Wrapper实现类型(无顺序)
    • cachedActivates : 当前Extension实现自动激活实现缓存(map,无序)
    • cachedNames : 扩展点实现类对应的名称(如配置多个名称则值为第一个)

    loadExtensionClasses方法执行完成之后,还有一下变量被赋值:

    • cachedDefaultName : 当前扩展点的默认实现名称

    getExtensionClasses方法执行完成之后,除了上述变量被赋值之外,还有以下变量被赋值:

    • cachedClasses : 扩展点实现名称对应的实现类(一个实现类可能有多个名称)

    其实也就是说,在调用了getExtensionClasses方法之后,当前扩展点对应的实现类的一些信息就已经加载进来了并且被缓存了。后面的许多操作都可以直接通过这些缓存数据来进行处理了。

    回到createAdaptiveExtension方法,他调用了getExtesionClasses方法加载扩展点实现信息完成之后,就可以直接通过判断cachedAdaptiveClass缓存字段是否被赋值盘确定当前扩展点是否有默认的AdaptiveExtension实现。如果没有,那么就调用createAdaptiveExtensionClass方法来动态生成一个。在dubbo的扩展点框架中大量的使用了缓存技术。

    创建自适应扩展点实现类型和实例化就已经完成了,下面就来看下扩展点自动注入的实现injectExtension

    private T injectExtension(T instance) {
        try {
            if (objectFactory != null) {
                for (Method method : instance.getClass().getMethods()) {
                    if (method.getName().startsWith("set")
                            && method.getParameterTypes().length == 1
                            && Modifier.isPublic(method.getModifiers())) {// 处理所有set方法
                        Class<?> pt = method.getParameterTypes()[0];// 获取set方法参数类型
                        try {
                            // 获取setter对应的property名称
                            String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
                            Object object = objectFactory.getExtension(pt, property); // 根据类型,名称信息从ExtensionFactory获取
                            if (object != null) { // 如果不为空,说set方法的参数是扩展点类型,那么进行注入
                                method.invoke(instance, object);
                            }
                        } catch (Exception e) {
                            logger.error("fail to inject via method " + method.getName()
                                    + " of interface " + type.getName() + ": " + e.getMessage(), e);
                        }
                    }
                }
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return instance;
    }

    这里可以看到,扩展点自动注入的一句就是根据setter方法对应的参数类型和property名称从ExtensionFactory中查询,如果有返回扩展点实例,那么就进行注入操作。到这里getAdaptiveExtension方法就分析完毕了。

    getExtension

    这个方法的主要作用是用来获取ExtensionLoader实例代表的扩展的指定实现。已扩展实现的名字作为参数,结合前面学习getAdaptiveExtension的代码,我们可以推测,这方法中也使用了在调用getExtensionClasses方法的时候收集并缓存的数据,其中涉及到名字和具体实现类型对应关系的缓存属性是cachedClasses。具体是是否如我们猜想的那样呢,学习一下相关代码就知道了:

    public T getExtension(String name) {
        if (name == null || name.length() == 0)
            throw new IllegalArgumentException("Extension name == null");
        if ("true".equals(name)) {  // 判断是否是获取默认实现
            return getDefaultExtension();
        }
        Holder<Object> holder = cachedInstances.get(name);// 缓存
        if (holder == null) {
            cachedInstances.putIfAbsent(name, new Holder<Object>());
            holder = cachedInstances.get(name);
        }
        Object instance = holder.get();
        if (instance == null) {
            synchronized (holder) {
                instance = holder.get();
                if (instance == null) {
                    instance = createExtension(name);// 没有缓存实例则创建
                    holder.set(instance);// 缓存起来
                }
            }
        }
        return (T) instance;
    }

    接着看createExtension方法的实现:

    private T createExtension(String name) {
        Class<?> clazz = getExtensionClasses().get(name); // getExtensionClass内部使用cachedClasses缓存
        if (clazz == null) {
            throw findException(name);
        }
        try {
            T instance = (T) EXTENSION_INSTANCES.get(clazz); // 从已创建Extension实例缓存中获取
            if (instance == null) {
                EXTENSION_INSTANCES.putIfAbsent(clazz, (T) clazz.newInstance());
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            }
            injectExtension(instance); // 属性注入
    
            // Wrapper类型进行包装,层层包裹
            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
            if (wrapperClasses != null && wrapperClasses.size() > 0) {
                for (Class<?> wrapperClass : wrapperClasses) {
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                }
            }
            return instance;
        } catch (Throwable t) {
            throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                    type + ")  could not be instantiated: " + t.getMessage(), t);
        }
    }

    从代码中可以看到,内部调用了getExtensionClasses方法来获取当前扩展的所有实现,而getExtensionClassse方法会在第一次被调用的时候将结果缓存到cachedClasses变量中,后面的调用就直接从缓存变量中获取了。这里还可以看到一个缓存EXTENSION_INSTANCES,这个缓存是ExtensionLoader的静态成员,也就是全局缓存,存放着所有的扩展点实现类型与其对应的已经实例化的实例对象(是所有扩展点,不是某一个扩展点),也就是说所有的扩展点实现在dubbo中最多都只会有一个实例。

    拿到扩展点实现类型对应的实例之后,调用了injectExtension方法对该实例进行扩展点注入,紧接着就是遍历该扩展点接口的所有Wrapper来对真正的扩展点实例进行Wrap操作,都是对通过将上一次的结果作为下一个Wrapper的构造函数参数传递进去实例化一个Wrapper对象,最后总返回回去的是Wrapper类型的实例而不是具体实现类的实例。

    这里或许有一个疑问: 从代码中看,不论instance是否存在于EXTENSION_INSTANCE,都会进行扩展点注入和Wrap操作。那么如果对于同一个扩展点,调用了两次createExtension方法的话,那不就进行了两次Wrap操作么?

    如果外部能够直接调用createExtension方法,那么确实可能出现这个问题。但是由于createExtension方法是private的,因此外部无法直接调用。而在ExtensionLoader类中调用它的getExtension方法(只有它这一处调用),内部自己做了缓存(cachedInstances),因此当getExtension方法内部调用了一次createExtension方法之后,后面对getExtension方法执行同样的调用时,会直接使用cachedInstances缓存而不会再去调用createExtension方法了。

    getActivateExtension

    getActivateExtension方法主要获取当前扩展的所有可自动激活的实现。可根据入参(values)调整指定实现的顺序,在这个方法里面也使用到getExtensionClasses方法中收集的缓存数据。

    public List<T> getActivateExtension(URL url, String[] values, String group) {
        List<T> exts = new ArrayList<T>();
        List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values); // 解析配置要使用的名称
    
        // 如果未配置"-default",则加载所有Activates扩展(names指定的扩展)
        if (! names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
            getExtensionClasses(); // 加载当前Extension所有实现,会获取到当前Extension中所有@Active实现,赋值给cachedActivates变量
            for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) { // 遍历当前扩展所有的@Activate扩展
                String name = entry.getKey();
                Activate activate = entry.getValue();
                if (isMatchGroup(group, activate.group())) { // 判断group是否满足,group为null则直接返回true
                    T ext = getExtension(name); // 获取扩展示例
    
                    // 排除names指定的扩展;并且如果names中没有指定移除该扩展(-name),且当前url匹配结果显示可激活才进行使用
                    if (! names.contains(name)
                            && ! names.contains(Constants.REMOVE_VALUE_PREFIX + name) 
                            && isActive(activate, url)) {
                        exts.add(ext);
                    }
                }
            }
            Collections.sort(exts, ActivateComparator.COMPARATOR); // 默认排序
        }
    
        // 对names指定的扩展进行专门的处理
        List<T> usrs = new ArrayList<T>();
        for (int i = 0; i < names.size(); i ++) { // 遍历names指定的扩展名
            String name = names.get(i);
            if (! name.startsWith(Constants.REMOVE_VALUE_PREFIX)
                    && ! names.contains(Constants.REMOVE_VALUE_PREFIX + name)) { // 未设置移除该扩展
                if (Constants.DEFAULT_KEY.equals(name)) { // default表示上面已经加载并且排序的exts,将排在default之前的Activate扩展放置到default组之前,例如:ext1,default,ext2
                    if (usrs.size() > 0) { // 如果此时user不为空,则user中存放的是配置在default之前的Activate扩展
                        exts.addAll(0, usrs); // 注意index是0,放在default前面
                        usrs.clear(); // 放到default之前,然后清空
                    }
                } else {
                    T ext = getExtension(name);
                    usrs.add(ext);
                }
            }
        }
        if (usrs.size() > 0) { // 这里留下的都是配置在default之后的
            exts.addAll(usrs); // 添加到default排序之后
        }
        return exts;
    }

    总结

    基本上将dubbo的扩展点加载机制学习了一遍,有几点可能需要注意的地方:

    • 每个ExtensionLoader实例只负责加载一个特定扩展点实现
    • 每个扩展点对应最多只有一个ExtensionLoader实例
    • 对于每个扩展点实现,最多只会有一个实例
    • 一个扩展点实现可以对应多个名称(逗号分隔)
    • 对于需要等到运行时才能决定使用哪一个具体实现的扩展点,应获取其自使用扩展点实现(AdaptiveExtension)
    • @Adaptive注解要么注释在扩展点@SPI的方法上,要么注释在其实现类的类定义上
    • 如果@Adaptive注解注释在@SPI接口的方法上,那么原则上该接口所有方法都应该加@Adaptive注解(自动生成的实现中默认为注解的方法抛异常)
    • 每个扩展点最多只能有一个被AdaptiveExtension
    • 每个扩展点可以有多个可自动激活的扩展点实现(使用@Activate注解)
    • 由于每个扩展点实现最多只有一个实例,因此扩展点实现应保证线程安全
    • 如果扩展点有多个Wrapper,那么最终其执行的顺序不确定(内部使用ConcurrentHashSet存储)

    TODO:

    • 学习一下动态生成AdaptiveExtension类的实现过程
      官方文档描述动态生成的AdaptiveExtension代码如下:
    package <扩展点接口所在包>;
    
    public class <扩展点接口名>$Adpative implements <扩展点接口> {
        public <有@Adaptive注解的接口方法>(<方法参数>) {
            if(是否有URL类型方法参数?) 使用该URL参数
            else if(是否有方法类型上有URL属性) 使用该URL属性
            # <else 在加载扩展点生成自适应扩展点类时抛异常,即加载扩展点失败!>
    
            if(获取的URL == null) {
                throw new IllegalArgumentException("url == null");
            }
    
            根据@Adaptive注解上声明的Key的顺序,从URL获致Value,作为实际扩展点名。
            如URL没有Value,则使用缺省扩展点实现。如没有扩展点, throw new IllegalStateException("Fail to get extension");
    
            在扩展点实现调用该方法,并返回结果。
        }
    
        public <有@Adaptive注解的接口方法>(<方法参数>) {
            throw new UnsupportedOperationException("is not adaptive method!");
        }
    }

    规则如下:

    • 先在URL上找@Adaptive注解指定的Extension名;
    • 如果不设置则缺省使用Extension接口类名的点分隔小写字串(即对于Extension接口com.alibaba.dubbo.xxx.YyyInvokerWrapper的缺省值为String[] {“yyy.invoker.wrapper”})。
    • 使用默认实现(@SPI指定),如果没有设定缺省扩展,则方法调用会抛出IllegalStateException。
    展开全文
  • 1.App Extension简介: 最近更新了iOS10,出来了许多新功能,UI的调整也是一大更新。通知栏的UI也进行了调整,记得之前下过一个在通知中心可以玩的小游戏Steve - The Jumping Dinosaur Widget Game,第一次玩的时候...

    1.App Extension简介:

    最近更新了iOS10,出来了许多新功能,UI的调整也是一大更新。通知栏的UI也进行了调整,记得之前下过一个在通知中心可以玩的小游戏Steve - The Jumping Dinosaur Widget Game,第一次玩的时候觉得非常神奇,游戏竟然能够在通知栏里面玩!


    通知中心小游戏

    还有现在通知中心可以显示越来越多的应用扩展,比如下面这个就是水哥手机的通知中心,有天气,中国移动流量等的快速查看。


    手机通知中心

    以前一直好奇这些功能是怎么实现的,因为项目里面没有碰到过,最近抽时间研究了一下,发现这其实就是本文要讲的App Extension的一种。

    App Extensiion是iOS8推出来的一个新特性,extension代表扩展的意思,也就是说我们可以在原有的app上添加一些扩展的内容和功能,但是这些功能并非是在我们的app中使用,而是通过其他的app,或者是iOS系统来使用的,比如我们上面“中国移动流量”的通知中心扩展,就是通过打开系统的通知中心来查看的,而不是在原本的移动掌上营业厅里面的。

    2.App Extension的常见类型

    应用扩展并不是只有我们上面所说的通知中心扩展,而是有很多种扩展点(Extension Point ,就是一种扩展类型,Apple为每一种扩展点分别加入了API, 所以应用扩展并不是只有一个API,而是每个扩展点都有自己的API),在iOS9,和iOS10中又相继推出了许多新的扩展点,说明这是一个趋势,个人感觉由于iOS系统的原因,很多东西不能够像安卓一样自定义,但是有了应用扩展,以后iOS功能也能越来越开放,灵活。相信应用扩展这个功能,以后许多人项目里面都应该用得上。

    iOS8中的扩展点推出来已经很久了,而且相应的功能也非常常用,所以这篇文章会介绍的比较详细,后面我也会写几篇单独的文章来介绍几个常用的扩展点。iOS9和iOS10的扩展也非常有用,但是工作中可能用的相对较少(其实是我不太熟悉。。),所以这篇文章里面介绍的可能稍微少点,但是后面我也会慢慢的去研究的!
    • iOS8中的扩展点:

    Today extensions (今日扩展):
    Today extension就是我们上面所说的通知中心扩展,因为这个扩展会显示在我们通知中心的 “今天” 这个标签下面。这个扩展的作用很简单,能够让用户更快速方便的看到app最及时的信息,比如中国移动的流量显示,我不用再每次打开移动的app去查看流量,而是直接在通知中心,甚至锁屏界面就可以查看,当然也可以像上面那个小游戏一样,在通知中心直接玩起来,而不用每次都去打开一个游戏程序。

    最近更新了一篇关于Today Extension如何实现的文章,有需要的可以看一下:
    iOS开发之App Extension(应用扩展)之 -- Today Extension

    Share extensions (分享扩展):
    分享扩展可以使用户在不同的app之间分享内容。这个功能在iOS5的时候就已经出来了,但是仅限于相册分享图片到tweeter,iOS6中可以分享到Facebook,但是现在,我们可以写分享扩展来分享到我们自己的服务器。
    比如我有一张相册中的图片想要通过微信发送给我的朋友,如果没有分享扩展,我只能打开微信与朋友聊天的界面 -> 选择发送图片 -> 到相册中选择图片 -> 然后发送。但是有了分享扩展,我可以直接在相册中点击分享按钮,点击微信,选择好友后,直接分享给好友,而不用打开微信来发送了。


    分享扩展

    Action extensions(行为扩展):
    行为扩展这个名字有点难理解,它可以让用户查看和改变一个app中的某些内容,而不用离开这个app。
    比如我在知乎看帖子,碰到一个不会的单词,咋办?如果没有应用扩展,我只能切换到有道词典,输入这个单词来查看,然后再切回知乎,但是现在有了有道词典的行为扩展,我只要复制这个单词,点击共享,在下面 选择有道词典的扩展,就可以不用打开有道词典这个app了,而直接能够显示出翻译结果。


    行为扩展

    Photo Editing extensions (图片编辑扩展):
    图片编辑扩展可以使用户直接在iphone的手机相册中利用第三方图片编辑软件提供的扩展来编辑图片。
    比如我现在有一张自拍照,想要编辑一下。如果没有图片编辑扩展的话,我只能打开美图秀秀之类的图片编辑软件,导入图片,编辑保存。有了图片编辑扩展之后,我只需要在系统相册中找到这张图片,点击分享按钮调出菜单,选择第三方的图片编辑扩展,就可以直接进入编辑界面,编辑完直接保存,而不用再打开这个图片编辑软件导入图片来进行编辑了。但是这个扩展仅限于在自带的相册中进行编辑,而不是所有app中图片都可以。


    图片编辑扩展

    Document Provider extensions (文件提供者扩展):
    文件提供者扩展会显示一个文件选择视图给用户,这些选择项可以让用户导入,导出,或者用其他app来打开这个文件。
    (这个扩展之前我的理解有问题,写的也是错误的,所以撤销了,网上关于这个扩展的资料非常少,后面补上吧)

    Custom Keyboard extensions(自定义键盘扩展):
    自定义键盘扩展可以让开发者创建系统键盘之外的自定义键盘,比如搜狗输入法。
    这个大家应该都很清楚了,水哥之前虽然没用过iphone,但是在iOS8之前应该是没有第三方输入法的,自从iOS增加了自定义键盘扩展之后,各种第三方输入法都蜂拥而至。

    • iOS9中的扩展点:
      网络相关的扩展点,很多的VPN,网络工具等软件都是基于这三个网络扩展点。

      Packet Tunnel Provider extension :
      可以利用这个扩展点来实现客户端的自定义VPN隧道协议。
      App Proxy Provider extension:
      利用这个扩展点可以实现客户端自定义透明网络代理协议。
      Filter Data Provider and the Filter Control Provider extension:
      利用这个扩展点可以实现动态的,基于设备的网络内容过滤。

      Safari相关的扩展点,很多的Safari广告屏蔽软件都是基于下面这两个扩展点

      Shared Links extension:
      利用这个扩展点可以使用户在Safari的分享链接里面看到app的内容
      Content Blocking extension :
      利用这个扩展点,可以给Safari提供一个拦截列表,在这个拦截列表里面你可以描述当用户再使用Safari的时候你想要拦截的内容。

      其他

      Index Maintenance extension:
      利用这个扩展点实现在不重启app的情况下对app内的数据重新建立索引。
      Audio Unit extension:
      这个扩展点允许你的应用提供乐器、声音效果、声音发生器等,它们可以在GarageBand、Logic这类AU宿主应用里使用。扩展点还可以将完整的音频插件模式搬到iOS上并允许你在App Store里销售Audio Units插件。

    • iOS10中的扩展点:

      Call Directory extension:
      Intents extension:
      Intents UI extension:
      Messages extension:
      Notification Content extension:
      Notification Service extension:
      Sticker Pack extension:

      iOS10中又新增了6个扩展点,这些扩展点的加入,使得iOS10功能更加强大。由于iOS10扩展的资料还比较少看到,我也没有做过相关的,所以我在网上找了一篇介绍的非常好的文章,如果对iOS10新增的扩展有兴趣的,大家可以去看看。iOS 10 应用扩展的剧变,对你的 iPhone 有什么影响?

    3.App Extension的工作原理,生命周期

    • 工作原理:

      应用扩展本身不是一个app,而只是对于某个app内容和功能的扩展,所以不能够单独的上架AppStore,而是跟随着你的app一起打包,这个包含应用扩展一起打包的app就叫做container app容器app)。虽然应用扩展是包含在container app中打包的,但是运行时它并不是跟你的app在同一个进程上面,而且有可能同一个app extension会同时运行在不同的进程,因为有可能同时有几个程序都打开了这个app extension,这个用来打开某个app Extension的应用就叫做host app宿主应用)。

      当一个应用扩展在运行的时候,它能够直接和host app进行通信,但是无法和container app进行通信,甚至经常在应用扩展运行的时候,你的container app可能都没有打开。比如对于微信分享扩展来说,如果我要从一个新闻软件分享一篇新闻到微信,通过微信的分享扩展,我可以不用打开微信,甚至微信的进程都没有启动,我只要在新闻软件中直接通过扩展分享到微信就可以了,下次打开微信就可以看到。如果一个app extension 一定要和container app 进行通信的时候,可以利用opeURL()或者是 数据共享 (本文只是概念基础介绍,后面会有单独的文章来介绍如何实现)。

    • 生命周期:

      因为应用扩展不是一个完整,独立的app,所以它的生命周期跟我们正常的app并不一样。应用扩展是在用户从其它软件的界面或者系统界面打开它的时候启动,一般都是host app发出一个request,app extension对应的响应这个请求,在response结束之后,app extension的生命周期也就终止了。

    4.Info.plist

    应用扩展创建之后会有自己的info.plist文件,info.plist文件中包含一个NSExtension作为key的字典,NSExtension中的内容根据每个扩展点的类型而各不相同,但是其中都必须包含NSExtensionPointIdentifier 这个key,对应的是扩展点的类型。NSExtension中还可以通过NSExtensionActivationRule 这个key对应的值来包含什么时候显示这些扩展的规则,通过这些规则,来判定用户什么时候会唤起你的扩展。 还有一个必须声明的是NSExtensionMainStoryboard 和NSExtensionPrincipalClass 中的某一个key,或者同时声明两个,代表的是用storyboard还是class来作为你的应用扩展入口。

    5.总结

    这篇文章很简单的介绍了一下App Extension(应用扩展)是什么,其实之所以会写这个是因为我在看到很多iOS软件的时候,总会觉得这些功能基于我现在的知识根本不能实现,那别人是怎么实现的呢?(比如通知中心小游戏,VPN软件,Safari广告屏蔽软件),在知道有应用扩展这个功能之后,我就恍然大悟了!所以,这篇文章主要就是给跟我一样,平时没有接触过App Extension的同学来大概的了解一下App Extension是什么,对于那些想要知道某个Extension Point具体实现的同学,可能就要看别的文章了,或者再等待一下,水哥马上会写几篇常用的扩展使用,每一篇都针对单独的一个Extension Point,尽量多写几个吧,因为我自己也很希望深入的了解一下App Extension。

    参考:
    《苹果官方文档》
    iOS新特性
    iOS 8 by Tutorials




    链接:http://www.jianshu.com/p/771c65731f37

    展开全文
  • Swift学习之路-Extension

    2019-02-27 19:27:43
    阅读该文章大约需要:15分钟读完之后你能获得:1、Extension是什么2、它能做什么 本文全部内容基于Swift版本:3.0.1 Extension的基本语法 extension SomeType { // new functionality to add to SomeType go...

    本文首发地址
    请在阅读本文章时,顺手将文中的示例代码在playground中敲一遍,这样能加深理解!!!
    阅读该文章大约需要:15分钟
    读完之后你能获得:
    1、Extension是什么
    2、它能做什么

    本文全部内容基于Swift版本:3.0.1

    Extension的基本语法

    extension SomeType {
        // new functionality to add to SomeType goes here
    }复制代码

    Tip:扩展可以为一个类型添加新的功能,但是不能重写已有的功能。

    struct Student {
        var name = ""
        var age = 1
        func print() {
    
        }
    }
    
    extension Student {
        //改行会报错`invalid redeclaration print()`重复声明print(),重写变量也是不行的。
        func print() {
        }
    }复制代码

    Swift中的Extension可以做什么

    我们想知道Extension在Swift中能做些什么,最直接的方法就是查看Swift的官方文档了。下面是文档中指出Extension能做的六个方面。

    • 添加计算型实例属性和计算型类型属性
    • 定义实例方法和类型方法
    • 提供新的构造器
    • 定义下标
    • 定义和使用新的嵌套类型
    • 使已存在的类型遵守某个协议

    看完上面Extension能做的六个方面,我们来逐条解释说明一下:

    添加计算型实例属性和计算型类型属性

    首先我们要了解什么是计算型属性,计算型属性(computed property)不直接存储值,而是提供一个getter和一个可选的setter,来间接获取和设置其他属性或变量的值。关于更多的计算型属性的内容请自行查看官方文档,在此不再赘述。那么我们什么场景可以用到这个功能呢?举一个最常见的例子,当你想访问某个view的width的时候,通常情况下你会这么写:

    view.frame.size.width复制代码

    但是这样写很长很不方便,作为一个懒惰的程序员,这时候你就要想我能不能缩短访问该属性的代码。不要犹豫了骚年,这时候你只需要写一个UIView的Extension就可以达到你的目的。

    extension UIView {
        var x: CGFloat {
            set {
                self.frame.origin.x = newValue
            }
            get {
                return self.frame.origin.x
            }
        }
    
        var y: CGFloat {
            set {
                self.frame.origin.y = newValue
            }
            get {
                return self.frame.origin.y
            }
        }
    
        var width: CGFloat {
            set {
                self.frame.size.width = newValue
            }
            get {
                return self.frame.size.width
            }
        }
        var height: CGFloat {
            set {
                self.frame.size.height = newValue
            }
            get {
                return self.frame.size.height
            }
        }
    }复制代码

    这样你就可以通过来访问该属性。怎么样,有没有感受到Extension的便利之处。

    view.width复制代码

    定义实例方法和类型方法

    在你辛辛苦苦写完一个Student类后,万恶的产品过来告诉你需求改了,这时虽然你心中有一万只草泥马在奔腾,但是为了心中那份神圣的程序员的责任感(当然还有糊口的工资),你还是要修改代码。如果你想在不改变原始类的基础上添加功能,那你可以给Student类添加Extension来解决问题。

    Tip:这里值得注意的一点是在Swift中,Extension可以给类和类型添加,比如你也可以给一个struct添加Extension,而在Objective-C中,你只能给类添加Extension。

    class Student {
        var name = ""
        var age = 1
    }
    
    extension Student {
        func printCurrentStudentName() {
            print(self.name)
        }
    }
    
    var jack = Student()
    jack.name = "jack"
    jack.printCurrentStudentName()复制代码

    提供新的构造器(Initializers)

    最常见的Rect通常由originsize来构造初始化,但是如果在你写完Rect的定义后,你偏偏想要通过center和size来确定Rect(作为一个程序员就要有一种作死的精神),那你就要用Extension来给Rect提供一个新的构造器。关于更多关于构造器的信息,请参考官方文档

    本例子来源官方文档

    struct Size {
        var width = 0.0, height = 0.0
    }
    struct Point {
        var x = 0.0, y = 0.0
    }
    struct Rect {
        var origin = Point()
        var size = Size()
    }
    
    let defaultRect = Rect()
    let memberwiseRect = Rect(origin: Point(x: 2.0, y: 2.0),
                              size: Size(width: 5.0, height: 5.0))复制代码

    通过Extension来给Rect添加一个新的构造器。

    extension Rect {
        init(center: Point, size: Size) {
            let originX = center.x - (size.width / 2)
            let originY = center.y - (size.height / 2)
            self.init(origin: Point(x: originX, y: originY), size: size)
        }
    }复制代码

    这样你就可以通过新的构造器来初始化Rect。

    let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
                          size: Size(width: 3.0, height: 3.0))
    // centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)复制代码

    定义下标

    通过Swift中的Extension,你可以给已知类型添加下标。例如下面的例子就是给Int类型添加一个下标,该下标表示十进制数从右向左的第n个数字。

    本例子来源官方文档

    extension Int {
        subscript(digitIndex: Int) -> Int {
            var decimalBase = 1
            for _ in 0..<digitIndex {
                decimalBase *= 10
            }
            return (self / decimalBase) % 10
        }
    }
    746381295[0]
    // 5
    746381295[1]
    // 9复制代码

    定义和使用新的嵌套类型(nest type)

    Extensions可以给已知的类、结构体、枚举添加嵌套类型。下面的例子是给Int类型添加一个判断正负数的Extension,该Extension嵌套一个枚举。

    本例子来源官方文档

    extension Int {
        enum Kind {
            case negative, zero, positive
        }
        var kind: Kind {
            switch self {
            case 0:
                return .zero
            case let x where x > 0:
                return .positive
            default:
                return .negative
            }
        }
    }
    
    func printIntegerKinds(_ numbers: [Int]) {
        for number in numbers {
            switch number.kind {
            case .negative:
                print("- ", terminator: "")
            case .zero:
                print("0 ", terminator: "")
            case .positive:
                print("+ ", terminator: "")
            }
        }
        print("")
    }
    printIntegerKinds([3, 19, -27, 0, -6, 0, 7])
    // Prints "+ + - 0 - 0 + "复制代码

    使已存在的类型遵守某个协议

    编写使该类型遵守某个协议的Extension的语法如下:

    extension SomeType: SomeProtocol, AnotherProtocol {
        // implementation of protocol requirements goes here
    }复制代码

    示例代码:

    protocol StudentProtocol {
        var address: String { get }
    }
    
    struct Student {
        var name = ""
        var age = 1
    
    }
    
    extension Student: StudentProtocol {
        var address: String {
            return "address"
        }
    }
    
    var jack = Student()
    jack.address
    //输出 address复制代码

    若添加Extension的类型已经实现协议中的内容,你可以写一个空的Extension来遵守协议:

    protocol StudentProtocol {
        var address: String { get }
    }
    
    struct Student {
        var address: String {
            return "address"
        }
        var name = ""
        var age = 1
    }
    
    extension Student: StudentProtocol {}
    
    var jack = Student()
    jack.address
    //输出 address复制代码

    总结

    • Extension可以为一个已有的类、结构体、枚举类型或者协议类型添加新功能。
    • 可以在没有权限获取原始源代码的情况下扩展类型的内容
    • Extendion和Objective-C中的Category类似。(OC中的Category有名字,Swift中的扩展没有名字)

    下篇预告:Swift-Protocol

    若本文有何错误或者不当之处,还望不吝赐教。谢谢!

    Swift-Extension的官方文档

    展开全文
  • extension(类扩展) 1、进行一个类扩展
  • Axure RP Extension for Chrome安装

    万次阅读 多人点赞 2018-08-14 14:31:52
    Axure RP Extension for Chrome安装 之前一直用 Firefox 浏览器浏览原型文件,一直用不惯,而且用 Firefox 的唯一目的就是看原型。其他都是用 Chrome 浏览器,来回切换,各种麻烦,然后下定决心解决 Chrome 浏览器...
  • 转载:http://blog.sina.com.cn/s/blog_788fd8560100vx03.html
  • 在用tomcat部署应用的时候,特别是在一个新服务器上部署一个应用的时候,我们经常会遇到如下类似的错误: Servlet.service() for servlet [springMVC] in context with path [] threw exception [Could not get ...
  • 问:CDR不能正常显示缩略图怎么办? 答:这是因为你在安装CDR时,对应版本的”ShellExt. msf“控件未正确安装,或者...那么在安装CDR时提示:Corel Graphics-Windows Shell Extension: The feature you are tryi...
  • postgres 创建 extension的时候报错,网上搜了好久终于找到了postgres=# create extension file_fdw;error: could not open extension control file "/usr/local/pgsql/share/extension/file_fdw.control"...
  • 2019独角兽企业重金招聘Python工程师标准>>> ...
  • torch.utils.cpp_extension.CppExtension(name, sources, *args, **kwargs) 创建一个C++的setuptools.Extension。 便捷地创建一个setuptools.Extension具有最小(但通常是足够)的参数来构建C++扩展的方法。 所有...
  • AXURE RP EXTENSION For Chrome 问题解决办法,实测成功

    万次阅读 多人点赞 2019-09-30 11:43:00
    首先下载AXURE RP EXTENSION For Chrome,很多人下载的都ctx格式的,然后拖进去安装,用了一次就崩溃了,主要还是安装方式不对,一般需要将下载的ctx格式修改成rar解压格式,然后解压出来成一个文件夹,然后安装...
  • 【本文参考】https://www.cnblogs.com/woods1815/p/9780683.html ...只需三步,不用下载Axure RP Extension for Chrome插件!!!只需三步,不用下载Axure RP Extension for Chrome插件!!!只需三步,不用下...
  • IIS下配置php运行环境。

    万次阅读 2018-06-06 10:58:46
    首先到php官网上下载php(http://php.net/downloads.php),版本根据自己的需要定。我用的是Current Stable...我用的是“VC15 x64 Non Thread Safe”非线程安全,下载的是Zip包。下载完后解压到自己的目录,然后进入目...
  • 安装Oracle VM VirtualBox Extension Pack

    万次阅读 2016-09-16 09:57:54
    VirtualBox,在菜单栏中找到“管理”–>“全局设定”–>“扩展” 选择对于版本的Oracle_VM_VirtualBox_Extension_Pack-xxxxx-xxxxxx.vbox-extpack安装即可 下载地址
  • win7配置IIS、集成PHP

    万次阅读 2012-06-18 08:39:36
    1. 安装iis,进入控制面板,将查看方式设为大图标,然后找到“程序和功能”,点击之,在左侧点击...  注:“Internet 信息服务”下的所有子集均可勾选,若出现安装不成功,则将以上两项勾除后重启电脑,然后先选择
  • 19个有用的gnome shell extension 扩展

    万次阅读 2012-03-30 12:54:00
    #1, Alternative tab extension :使用ALT+TAB 经典模式 1sudo apt-get install gnome-shell-extensions-alternate-tab #2, Alternative Status Menu extension :在状态栏添加“Power off”与...
  • Oracle VM VirtualBox Extension Pack 下载

    千次阅读 2019-01-12 17:10:51
    VirtualBox 5.2.12 Oracle VM VirtualBox Extension Pack下载 其这版本下载方法相同,自行选择   1 首先打开官网: https://www.virtualbox.org/wiki/Downloads 2 ,向下找到"VirtualBox older builds"...
  • MIME大全

    千次阅读 2013-08-05 16:22:37
    - <!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding co
1 2 3 4 5 ... 20
收藏数 327,703
精华内容 131,081
关键字:

extension