热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

iOS:WebKit内核框架的应用与解析

原文:http:www.cnblogs.comfengminp5737355.html一、摘要:WebKit是iOS8之后引入的专门负责处理网页视图的框架,其比UIWebView更加

原文:http://www.cnblogs.com/fengmin/p/5737355.html

一、摘要:

WebKit是iOS8之后引入的专门负责处理网页视图的框架,其比UIWebView更加强大,性能也更优。

二、引言:

在iOS8之前,在应用中嵌入网页通常需要使用UIWebView这样一个类,这个类通过URL或者HTML文件来加载网页视图,功能十分有限,只能作为辅助嵌入原生应用程序中。虽然UIWebView也可以做原生与Javascript交互的相关处理,然而也有很大的局限性,Javascript要调用原生方法通常需要约定好协议之后通过Request来传递。WebKit框架中添加了一些原生与Javascript交互的方法,增强了网页视图与原生的交互能力。并且WebKit框架中采用导航堆栈的模型来管理网页的跳转,开发者也可以更加容易的控制和管理网页的渲染。

三、WebKit框架概览:

WebKit框架中涉及的类很多,框架的设计十分面向对象和模块化,开发者在使用时可以轻松的写出结构清晰的代码。在进行使用前,我们首先应该清楚整个框架的结构和开发思路,下面一张脑图中基本列出了WebKit框架中所涉及到的所有重要的类以及他们之间的相互关系:

如上图所示,WebKit框架中最核心的类应该属于WKWebView了,这个类专门用来渲染网页视图,其他类和协议都将基于它和服务于它。

WKWebView:网页的渲染与展示,通过WKWebViewConfiguration可以进行配置。

WKWebViewConfiguration:这个类专门用来配置WKWebView。

WKPreference:这个类用来进行M相关设置。

WKProcessPool:这个类用来配置进程池,与网页视图的资源共享有关。

WKUserContentController:这个类主要用来做native与Javascript的交互管理。

WKUserScript:用于进行Javascript注入。

WKScriptMessageHandler:这个类专门用来处理Javascript调用native的方法。

WKNavigationDelegate:网页跳转间的导航管理协议,这个协议可以监听网页的活动。

WKNavigationAction:网页某个活动的示例化对象。

WKUIDelegate:用于交互处理Javascript中的一些弹出框。

WKBackForwardList:堆栈管理的网页列表。

WKBackForwardListItem:每个网页节点对象。

使用WKWebViewConfiguration对WebView进行配置:

使用下面的代码可以创建一个WKWebView视图,创建WebView视图时,需要使用WKWebViewConfiguration来进行配置。

// webkit内核中的网页视图,类似于UIWebView
WKWebView * WK; WKWebViewConfiguration * cOnfig= [[WKWebViewConfiguration alloc]init]; WK = [[WKWebView alloc]initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height-40) configuration:config]; [WK loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]]];

WKWebViewConfiguration中可以进行配置的方法和属性如下:

1、WKProcessPool类中没有暴露任何属性和方法,配置为同一个进程池的WebView会共享数据,例如COOKIE、用户凭证等,开发者可以通过编写管理类来分配不同维度的WebView在不同进程池中。

//设置进程池
WKProcessPool * pool = [[WKProcessPool alloc]init]; config.processPool = pool;

2、WKPerference实例为WebView提供一个偏好设置。

//进行偏好设置
WKPreferences * preference = [[WKPreferences alloc]init]; //最小字体大小 当将JavascriptEnabled属性设置为NO时,可以看到明显的效果
preference.minimumFOntSize= 0; //设置是否支持Javascript 默认是支持的
preference.JavascriptEnabled = YES; //设置是否允许不经过用户交互由Javascript自动打开窗口
preference.JavascriptCanOpenWindowsAutomatically = YES; config.preferences = preference;

3、WKUserContentController专门用来管理native与Javascript的交互行为,addScriptMessageHandler:name:方法来注册要被js调用的方法名称,之后再Javascript中使用window.webkit.messageHandlers.name.postMessage()方法来像native发送消息,支持OC中字典,数组,NSNumber等原生数据类型,Javascript代码中的name要和上面注册的相同:

//设置内容交互控制器 用于处理Javascript与native交互
WKUserContentController * userCOntroller= [[WKUserContentController alloc]init]; //设置处理代理并且注册要被js调用的方法名称
[userController addScriptMessageHandler:self name:@"name"]; //js注入,注入一个测试方法。
NSString *JavascriptSource = @"function userFunc(){window.webkit.messageHandlers.name.postMessage( {\"name\":\"HS\"})}"; WKUserScript *userScript = [[WKUserScript alloc] initWithSource:JavascriptSource injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]; // forMainFrameOnly:NO(全局窗口),yes(只限主窗口)[userController addUserScript:userScript];
config.userCOntentController= userController;

4、在native代理的回调方法中,会获取到Javascript传递进来的消息,如下:

-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{ //这里可以获取到Javascript传递进来的消息
}

WKScriptMessage类是Javascript传递的对象实例,其中属性如下:

//传递的消息主体
@property (nonatomic, readonly, copy) id body; //传递消息的WebView
@property (nullable, nonatomic, readonly, weak) WKWebView *webView; //传递消息的WebView当前页面对象
@property (nonatomic, readonly, copy) WKFrameInfo *frameInfo; //消息名称
@property (nonatomic, readonly, copy) NSString *name;

WKUserContentController实例的addUserScript:用于注入Javascript代码,后面会专门介绍。

WebKit框架采用其本身的缓存框架,WKWebsiteDataStore类用来处理数据的存储,其中属性和方法如下:

//设置数据存储store
config.websiteDataStore = [WKWebsiteDataStore defaultDataStore];
@interface WKWebsiteDataStore : NSObject //获取默认的存储器 此存储器为持久性的会被写入磁盘
+ (WKWebsiteDataStore *)defaultDataStore; //获取一个临时的存储器
+ (WKWebsiteDataStore *)nonPersistentDataStore; //存储器是否是临时的
@property (nonatomic, readonly, getter=isPersistent) BOOL persistent; //所有可以存储的类型
+ (NSSet *)allWebsiteDataTypes; @end
//设置是否将网页内容全部加载到内存后再渲染
    config.suppressesIncrementalRendering = NO;    //设置HTML5视频是否允许网页播放 设置为NO则会使用本地播放器
    config.allowsInlineMediaPlayback =  YES;    //设置是否允许ariPlay播放
    config.allowsAirPlayForMediaPlayback = YES;    //设置视频是否需要用户手动播放 设置为NO则会允许自动播放
    config.requiresUserActiOnForMediaPlayback= NO;    //设置是否允许画中画技术 在特定设备上有效
    config.allowsPictureInPictureMediaPlayback = YES;    //设置选择模式 是按字符选择 还是按模块选择/*
 typedef NS_ENUM(NSInteger, WKSelectionGranularity) { //按模块选择
 WKSelectionGranularityDynamic, //按字符选择
 WKSelectionGranularityCharacter, } NS_ENUM_AVAILABLE_IOS(8_0); */ config.selectionGranularity = WKSelectionGranularityCharacter;    //设置请求的User-Agent信息中应用程序名称 iOS9后可用
    config.applicatiOnNameForUserAgent= @"HS";

WKWebView中的属性和方法解析

下面列举了WKWebView中常用的属性和方法:

//设置导航代理
@property (nullable, nonatomic, weak) id  navigationDelegate; //设置UI代理
@property (nullable, nonatomic, weak) id  UIDelegate; //导航列表
@property (nonatomic, readonly, strong) WKBackForwardList *backForwardList; //通过url加载网页视图
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request; //通过文件加载网页视图
- (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL NS_AVAILABLE(10_11, 9_0); //通过HTML字符串加载网页视图
- (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL; //通过data数据加载网页视图
- (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL NS_AVAILABLE(10_11, 9_0); //渲染导航列表中的某个网页节点
- (nullable WKNavigation *)goToBackForwardListItem:(WKBackForwardListItem *)item; //网页标题
@property (nullable, nonatomic, readonly, copy) NSString *title; //网页的url
@property (nullable, nonatomic, readonly, copy) NSURL *URL; //网页是否正在加载中
@property (nonatomic, readonly, getter=isLoading) BOOL loading; //加载进度 可以监听这个属性的值配合UIProgressView来设计进度条
@property (nonatomic, readonly) double estimatedProgress; //是否全部是安全连接
@property (nonatomic, readonly) BOOL hasOnlySecureContent; //证书列表
@property (nonatomic, readonly, copy) NSArray *certificateChain; //是否可以回退
@property (nonatomic, readonly) BOOL canGoBack; //是否可以前进
@property (nonatomic, readonly) BOOL canGoForward; //回退网页
- (nullable WKNavigation *)goBack; //前进网页
- (nullable WKNavigation *)goForward; //刷新网页
- (nullable WKNavigation *)reload; //忽略缓存的刷新
- (nullable WKNavigation *)reloadFromOrigin; //停止加载
- (void)stopLoading; //执行Javascript代码
- (void)evaluateJavascript:(NSString *)JavascriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler; //是否允许右滑返回手势
@property (nonatomic) BOOL allowsBackForwardNavigationGestures;

WKBackForwardList类为导航管理的网页列表类,其中属性方法意义如下:

@interface WKBackForwardList : NSObject //当前所在的网页节点
@property (nullable, nonatomic, readonly, strong) WKBackForwardListItem *currentItem; //前进的一个网页节
@property (nullable, nonatomic, readonly, strong) WKBackForwardListItem *forwardItem; //回退的一个网页节点
@property (nullable, nonatomic, readonly, strong) WKBackForwardListItem *backItem; //获取某个index的网页节点
- (nullable WKBackForwardListItem *)itemAtIndex:(NSInteger)index; //获取回退的节点数组
@property (nonatomic, readonly, copy) NSArray *backList; //获取前进的节点数组
@property (nonatomic, readonly, copy) NSArray *forwardList; @end

在WebKit中,网页节点被抽象成为了WKBackForwardListItem类,这个类中封装的属性如下:

@interface WKBackForwardListItem : NSObject //当前节点的URL
@property (readonly, copy) NSURL *URL; //当前节点的标题
@property (nullable, readonly, copy) NSString *title; //创建此WebView的初始URL
@property (readonly, copy) NSURL *initialURL;

关于native与Javascript交互

WebKit中的native与Javascript的交互主要有4类。

1.Javascript调用native方法

这种方式是由WKUserContentController注册,并在代理方法中实现的。

2.native调用Javascript方法

这种方式通过WKWebView直接调用evaluteJavascript:completionHandler:方法来实现。

3.将Javascript代码注入

这种方式可以在网页中注入一些自定义的Javascript代码,也可以注入自定义的方法,再使用evaluteJavascript:completionHandler:来调用方法。Javascript代码的注入也是通过WKUserContentController来完成的,使用addUserScript:方法来注入Javascript,其中需要通过WKUserScript类来生成要注入的对象,这个类使用如下方法来进行实例化:

/*source为要注入的js代码 WKUserScriptInjectionTime设置注入的时机forMainFrameOnly参数设置是否只在主页面注入 typedef NS_ENUM(NSInteger, WKUserScriptInjectionTime) { //原js代码运行前注入 WKUserScriptInjectionTimeAtDocumentStart, //原js代码运行后注入 WKUserScriptInjectionTimeAtDocumentEnd } NS_ENUM_AVAILABLE(10_10, 8_0); */

- (instancetype)initWithSource:(NSString *)source injectionTime:(WKUserScriptInjectionTime)injectionTime forMainFrameOnly:(BOOL)forMainFrameOnly;

4.通过WKUIDelegate来交互

这种方式主要用于相应Javascript中的弹出框,后面会详细介绍这个协议。

WKNavagationDelegate中方法解析

WKNavagationDelegate协议重要有两个作用,监听页面渲染流程与控制页面跳转,其中方法如下:

/* 决定是否响应网页的某个动作,例如加载,回退,前进,刷新等,在这个方法中,必须执行decisionHandler()代码块,并将是否允许这个活动执行在block中进行传入 *//* WKNavigationAction是网页动作的抽象化,其中封装了许多行为信息,后面会介绍 WKNavigationActionPolicy为开发者回执,枚举如下: typedef NS_ENUM(NSInteger, WKNavigationActionPolicy) { //取消此次行为 WKNavigationActionPolicyCancel, //允许此次行为 WKNavigationActionPolicyAllow, } NS_ENUM_AVAILABLE(10_10, 8_0); */
-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{ decisionHandler(WKNavigationActionPolicyAllow); } //需要响应身份验证时调用 同样在block中需要传入用户身份凭证
-(void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler{ //用户身份信息
    NSURLCredential *newCred = [NSURLCredential credentialWithUser:@"" password:@"" persistence:NSURLCredentialPersistenceNone]; // 为 challenge 的发送方提供 credential [[challenge sender] useCredential:newCred
 forAuthenticationChallenge:challenge]; completionHandler(NSURLSessionAuthChallengeUseCredential,newCred); } //接收到数据后是否允许执行渲染/*其中,WKNavigationResponse为请求回执信息
WKNavigationResponsePokicy为开发者回执,枚举如下: typedef NS_ENUM(NSInteger, WKNavigationResponsePolicy) { //取消渲染 WKNavigationResponsePolicyCancel, //允许渲染 WKNavigationResponsePolicyAllow,
} NS_ENUM_AVAILABLE(10_10, 8_0); */
-(void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{ decisionHandler(WKNavigationResponsePolicyAllow); } //=====================下面这个协议方法用于监听流程=========================================//页面加载启动时调用-(void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation{
} //当主机接收到的服务重定向时调用

-(void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation{ } //内容到达主机时调用

-(void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation{ } //主页加载完成时调用

-(void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{ } //提交发生错误时调用

-(void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error{ } //主页数据加载发生错误时调用

-(void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(nonnull NSError *)error{ } //进程被终止时调用

-(void)webViewWebContentProcessDidTerminate:(WKWebView *)webView{ }

WKUIDelegate协议中方法解析:

//创建新的webView时调用的方法
-(WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures{    return webView; } //关闭webView时调用的方法
-(void)webViewDidClose:(WKWebView *)webView{ } //下面这些方法是交互Javascript的方法 //Javascript调用alert方法后回调的方法 message中为alert提示的信息 必须要在其中调用completionHandler()
-(void)webView:(WKWebView *)webView runJavascriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{    NSLog(@"%@",message); completionHandler(); } //Javascript调用confirm方法后回调的方法 confirm是js中的确定框,需要在block中把用户选择的情况传递进去
-(void)webView:(WKWebView *)webView runJavascriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{    NSLog(@"%@",message); completionHandler(YES); } //Javascript调用prompt方法后回调的方法 prompt是js中的输入框 需要在block中把用户输入的信息传入
-(void)webView:(WKWebView *)webView runJavascriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler{    NSLog(@"%@",prompt); completionHandler(@"123"); }

扩展

首先,在注册要被Javascript调用的方法时需要设置代理,在不需要时需要将代理移除,WKUserContentController中也提供了移除这个代理的方法,如果不移除,将会造成WebView不能释放。方法如下:

//注册一个监听方法
- (void)addScriptMessageHandler:(id )scriptMessageHandler name:(NSString *)name;//移除一个方法的监听- (void)removeScriptMessageHandlerForName:(NSString *)name;同样与注入Javascript对应,也可以将注入的代码移除,方法如下: //注入一个Javascript抽象对象
- (void)addUserScript:(WKUserScript *)userScript; //移除所有注入
- (void)removeAllUserScripts;

在上面,经常会见到WKNavagationAction这个类,这个类中封装的是一些页面活动信息,如下:

@interface WKNavigationAction : NSObject //原页面
@property (nonatomic, readonly, copy) WKFrameInfo *sourceFrame; //目标页面
@property (nullable, nonatomic, readonly, copy) WKFrameInfo *targetFrame; //请求URL
@property (nonatomic, readonly, copy) NSURLRequest *request; //活动类型/*typedef NS_ENUM(NSInteger, WKNavigationType) { //链接激活 WKNavigationTypeLinkActivated, //提交操作 WKNavigationTypeFormSubmitted, //前进操作 WKNavigationTypeBackForward, //刷新操作 WKNavigationTypeReload, //重提交操作 例如前进 后退 刷新 WKNavigationTypeFormResubmitted, //其他类型
    WKNavigatiOnTypeOther= -1, } NS_ENUM_AVAILABLE(10_10, 8_0); */ @property (nonatomic, readonly) WKNavigationType navigationType; @end

 一个强大的JS与OC交互的框架:https://github.com/marcuswestin/WebViewJavascriptBridge


推荐阅读
  • Nginx使用AWStats日志分析的步骤及注意事项
    本文介绍了在Centos7操作系统上使用Nginx和AWStats进行日志分析的步骤和注意事项。通过AWStats可以统计网站的访问量、IP地址、操作系统、浏览器等信息,并提供精确到每月、每日、每小时的数据。在部署AWStats之前需要确认服务器上已经安装了Perl环境,并进行DNS解析。 ... [详细]
  • 原文地址:https:www.cnblogs.combaoyipSpringBoot_YML.html1.在springboot中,有两种配置文件,一种 ... [详细]
  • Webmin远程命令执行漏洞复现及防护方法
    本文介绍了Webmin远程命令执行漏洞CVE-2019-15107的漏洞详情和复现方法,同时提供了防护方法。漏洞存在于Webmin的找回密码页面中,攻击者无需权限即可注入命令并执行任意系统命令。文章还提供了相关参考链接和搭建靶场的步骤。此外,还指出了参考链接中的数据包不准确的问题,并解释了漏洞触发的条件。最后,给出了防护方法以避免受到该漏洞的攻击。 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • 本文介绍了绕过WAF的XSS检测机制的方法,包括确定payload结构、测试和混淆。同时提出了一种构建XSS payload的方法,该payload与安全机制使用的正则表达式不匹配。通过清理用户输入、转义输出、使用文档对象模型(DOM)接收器和源、实施适当的跨域资源共享(CORS)策略和其他安全策略,可以有效阻止XSS漏洞。但是,WAF或自定义过滤器仍然被广泛使用来增加安全性。本文的方法可以绕过这种安全机制,构建与正则表达式不匹配的XSS payload。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 小程序自动授权和手动接入的方式及操作步骤
    本文介绍了小程序支持的两种接入方式:自动授权和手动接入,并详细说明了它们的操作步骤。同时还介绍了如何在两种方式之间切换,以及手动接入后如何下载代码包和提交审核。 ... [详细]
  • 本文介绍了响应式页面的概念和实现方式,包括针对不同终端制作特定页面和制作一个页面适应不同终端的显示。分析了两种实现方式的优缺点,提出了选择方案的建议。同时,对于响应式页面的需求和背景进行了讨论,解释了为什么需要响应式页面。 ... [详细]
  • 本文介绍了RxJava在Android开发中的广泛应用以及其在事件总线(Event Bus)实现中的使用方法。RxJava是一种基于观察者模式的异步java库,可以提高开发效率、降低维护成本。通过RxJava,开发者可以实现事件的异步处理和链式操作。对于已经具备RxJava基础的开发者来说,本文将详细介绍如何利用RxJava实现事件总线,并提供了使用建议。 ... [详细]
  • Java和JavaScript是什么关系?java跟javaScript都是编程语言,只是java跟javaScript没有什么太大关系,一个是脚本语言(前端语言),一个是面向对象 ... [详细]
author-avatar
何卫先生
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有