從原始碼入手來學習EventBus 3事件匯流排機制
3月28日訊息,二次元網站AcFun.com(A站)的本輪融資幾經波折仍未落定,這一次又將有新的玩家進入。據《財經》記者從接近A站人士處獲悉,雲鋒基金及其背後的阿里巴巴目前已經放棄控股A站。取而代之的是,最近在長視訊與二次元領域動作不斷的今日頭條有意接手A站。
作者簡介大家早上好,不知不覺已經到了週四,再堅持一下就要到週末了!
本篇來自 Xu 的投稿,分享了他對 EventBus 3.1.1 的原始碼分析,一起來看看!希望大家喜歡。
Xu 的部落格地址:
前言https://xudeveloper.github.io/
我研究 EventBus 原始碼的目的是為了解決以下幾個我在使用過程中所思考的問題:
這個框架涉及到一種設計模式叫做觀察者模式,什麼是觀察者模式?
事件如何進行定義,有沒有相關限制?
觀察者繫結觀察事件的時候,繫結方法的命名有限制嗎?
事件傳送和接收的原理?
為了研究原始碼的方便,我寫了一個簡單的 demo。
定義事件
TestEvent.java:
public class TestEvent {
private String msg;
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
主 Activity
MainActivity.java:
public class MainActivity extends AppCompatActivity {
private Button button;
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.button);
textView = findViewById(R.id.text);
EventBus.getDefault().register(this );
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
TestEvent event = new TestEvent();
event.setMsg("已接收到事件!");
EventBus.getDefault().post(event);
}
});
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onTestEvent(TestEvent event) {
textView.setText(event.getMsg());
}
@Override
protected void onDestroy() {
EventBus.getDefault().unregister(this);
super.onDestroy();
}
}
執行效果
原始碼分析關於觀察者模式
簡介:觀察者模式是設計模式中的一種。它是為了定義物件間的一種一對多的依賴關係,即當一個物件的狀態發生改變時,所有依賴於它的物件都得到通知並被自動更新。
如何使用:這裡傳送門有相關的demo,這裡不再詳述。
重點:在這個模式中主要包含兩個重要的角色:釋出者和訂閱者(又稱觀察者)。對應 EventBus 來說,釋出者即傳送訊息的一方(即呼叫EventBus.getDefault().post(event) 的一方),訂閱者即接收訊息的一方(即呼叫EventBus.getDefault().register() 的一方)。
我們已經解決了第一個問題~
關於事件
這裡指的事件其實是一個泛泛的統稱,指的是一個概念上的東西(當時我還以為一定要以啥 Event 命名…),通過查閱官方文件,我知道事件的命名格式並沒有任何要求,你可以定義一個物件作為事件,也可以傳送基本資料型別如 int,String 等作為一個事件。後續的原始碼分析我也會再次證明一下。
具體分析
從函式入口開始分析:
EventBus#getDefault()
public static EventBus getDefault() {
if (defaultInstance == null) {
synchronized (EventBus.class) {
if (defaultInstance == null) {
defaultInstance = new EventBus();
}
}
}
return defaultInstance;
}
這裡就是採用雙重校驗並加鎖的單例模式生成 EventBus 例項。
public void register(Object subscriber) {
Class<?> subscriberClass = subscriber.getClass();
List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
subscribe(subscriber, subscriberMethod);
}
}
}
由於我們傳入的為 this,即 MainActivity 的例項,所以第一行程式碼獲取了訂閱者的 class 物件,然後會找出所有訂閱的方法。我們看一下第二行的邏輯。
SubscriberMethodFinder#findSubscriberMethods():
List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
if (subscriberMethods != null) {
return subscriberMethods;
}
if (ignoreGeneratedIndex) {
subscriberMethods = findUsingReflection(subscriberClass);
} else {
subscriberMethods = findUsingInfo(subscriberClass);
}
if (subscriberMethods.isEmpty()) {
throw new EventBusException("Subscriber " + subscriberClass + " and its super classes have no public methods with the @Subscribe annotation");
} else {
METHOD_CACHE.put(subscriberClass, subscriberMethods);
return subscriberMethods;
}
}
分析:
如果快取中有對應 class 的訂閱方法列表,則直接返回,這裡我們是第一次建立,所以此時 subscriberMethods 為空;
接下來會有一個引數判斷,通過檢視前面的建立過程,ignoreGeneratedIndex 預設為false,進入 else 程式碼塊,後面生成 subscriberMethods 成功的話會加入到快取中,失敗的話會 throw 異常。
SubscriberMethodFinder#findUsingInfo()
private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
// 2.1
FindState findState = prepareFindState();
findState.initForSubscriber(subscriberClass);
// 2.2
while (findState.clazz != null) {
findState.subscriberInfo = getSubscriberInfo(findState);
if (findState.subscriberInfo != null) {
SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
for (SubscriberMethod subscriberMethod: array) {
if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
findState.subscriberMethods.add(subscriberMethod);
}
}
} else {
// 2.3
findUsingReflectionInSingleClass(findState);
}
findState.moveToSuperclass();
}
// 2.4
return getMethodsAndRelease(findState);
}
SubscriberMethodFinder#prepareFindState()
private static final FindState[] FIND_STATE_POOL = new FindState[POOL_SIZE];
private FindState prepareFindState() {
synchronized(FIND_STATE_POOL) {
for (int i = 0; i < POOL_SIZE; i++) {
FindState state = FIND_STATE_POOL[i];
if (state != null) {
FIND_STATE_POOL[i] = null;
return state;
}
}
}
return new FindState();
}
這個方法是建立一個新的 FindState 類,通過兩種方法獲取,一種是從 FIND_STATE_POOL 即 FindState 池中取出可用的 FindState,如果沒有的話,則通過第二種方式:直接 new 一個新的 FindState 物件。
FindState#initForSubscriber()
static class FindState {
// 省略程式碼
void initForSubscriber(Class<?> subscriberClass) {
this.subscriberClass = clazz = subscriberClass;
skipSuperClasses = false;
subscriberInfo = null;
}
// 省略程式碼
}
FindState 類是 SubscriberMethodFinder 的內部類,這個方法主要做一個初始化的工作。
SubscriberMethodFinder#getSubscriberInfo()
private SubscriberInfo getSubscriberInfo(FindState findState) {
if (findState.subscriberInfo != null && findState.subscriberInfo.getSuperSubscriberInfo() != null) {
SubscriberInfo superclassInfo = findState.subscriberInfo.getSuperSubscriberInfo();
if (findState.clazz == superclassInfo.getSubscriberClass()) {
return superclassInfo;
}
}
if (subscriberInfoIndexes != null) {
for (SubscriberInfoIndex index: subscriberInfoIndexes) {
SubscriberInfo info = index.getSubscriberInfo(findState.clazz);
if (info != null) {
return info;
}
}
}
return null;
}
這裡由於初始化的時候,findState.subscriberInfo和subscriberInfoIndexes 為空,所以這裡直接返回 null,後續我們可以再回到這裡看看 subscriberInfo 有什麼作用。
SubscriberMethodFinder#findUsingReflectionInSingleClass()
private void findUsingReflectionInSingleClass(FindState findState) {
Method[] methods;
try {
// This is faster than getMethods, especially when subscribers are fat classes like Activities
methods = findState.clazz.getDeclaredMethods();
} catch (Throwable th) {
// Workaround for java.lang.NoClassDefFoundError, see https://github.com/greenrobot/EventBus/issues/149
methods = findState.clazz.getMethods();
findState.skipSuperClasses = true;
}
for (Method method: methods) {
int modifiers = method.getModifiers();
if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
Class<?> [] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 1) {
Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
if (subscribeAnnotation != null) {
// !!!
Class<?> eventType = parameterTypes[0];
if (findState.checkAdd(method, eventType)) {
ThreadMode threadMode = subscribeAnnotation.threadMode();
findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode, subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
}
}
} else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
String methodName = method.getDeclaringClass().getName() + "." + method.getName();
throw new EventBusException("@Subscribe method " + methodName + "must have exactly 1 parameter but has " + parameterTypes.length);
}
} else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
String methodName = method.getDeclaringClass().getName() + "." + method.getName();
throw new EventBusException(methodName + " is a illegal @Subscribe method: must be public, non-static, and non-abstract");
}
}
}
這個方法的邏輯是:
通過反射的方式獲取訂閱者類中的所有宣告方法,然後在這些方法裡面尋找以 @Subscribe作為註解的方法進行處理(!!!部分的程式碼),先經過一輪檢查,看看 findState.subscriberMethods是否存在,如果沒有的話,將方法名,threadMode,優先順序,是否為 sticky 方法封裝為 SubscriberMethod 物件,新增到 subscriberMethods 列表中。
什麼是 sticky event?
sticky event,中文名為粘性事件。普通事件是先註冊,然後傳送事件才能收到;而粘性事件,在傳送事件之後再訂閱該事件也能收到。此外,粘性事件會儲存在記憶體中,每次進入都會去記憶體中查詢獲取最新的粘性事件,除非你手動解除註冊。
在這裡我們解決了第二個和第三個問題,方法的命名並沒有任何要求,只是加上 @Subscribe 註解即可!同時事件的命名也沒有任何要求!
之後這個 while 迴圈會繼續檢查父類,當然遇到系統相關的類時會自動跳過,以提升效能。
SubscriberMethodFinder#getMethodsAndRelease
private List<SubscriberMethod> getMethodsAndRelease(FindState findState) {
List<SubscriberMethod> subscriberMethods = new ArrayList<>(findState.subscriberMethods);
findState.recycle();
synchronized(FIND_STATE_POOL) {
for (int i = 0; i < POOL_SIZE; i++) {
if (FIND_STATE_POOL[i] == null) {
FIND_STATE_POOL[i] = findState;
break;
}
}
}
return subscriberMethods;
}
這裡將 subscriberMethods 列表直接返回,同時會把 findState 做相應處理,儲存在 FindState 池中,方便下一次使用,提高效能。
EventBus#subscribe()
返回 subscriberMethods 之後,register 方法的最後會呼叫 subscribe 方法:
public void register(Object subscriber) {
Class<?> subscriberClass = subscriber.getClass();
List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
subscribe(subscriber, subscriberMethod);
}
}
}
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
Class<?> eventType = subscriberMethod.eventType;
Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
if (subscriptions == null) {
subscriptions = new CopyOnWriteArrayList <> ();
subscriptionsByEventType.put(eventType, subscriptions);
} else {
if (subscriptions.contains(newSubscription)) {
throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event " + eventType);
}
}
int size = subscriptions.size();
for (int i = 0; i <= size; i++) {
if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
subscriptions.add(i, newSubscription);
break;
}
}
List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
if (subscribedEvents == null) {
subscribedEvents = new ArrayList<>();
typesBySubscriber.put(subscriber, subscribedEvents);
}
subscribedEvents.add(eventType);
if (subscriberMethod.sticky) {
if (eventInheritance) {
// Existing sticky events of all subclasses of eventType have to be considered.
// Note: Iterating over all events may be inefficient with lots of sticky events,
// thus data structure should be changed to allow a more efficient lookup
// (e.g. an additional map storing sub classes of super classes: Class -> List<Class>).
Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();
for (Map.Entry<Class<?>, Object> entry : entries) {
Class<?> candidateEventType = entry.getKey();
if (eventType.isAssignableFrom(candidateEventType)) {
Object stickyEvent = entry.getValue();
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
} else {
Object stickyEvent = stickyEvents.get(eventType);
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
}
分析:
首先,根據 subscriberMethod.eventType(在 Demo 裡面指的是 TestEvent),在 subscriptionsByEventType 去查詢一個 CopyOnWriteArrayList ,如果沒有則建立一個新的 CopyOnWriteArrayList;
然後將這個 CopyOnWriteArrayList 放入 subscriptionsByEventType 中,這裡的 subscriptionsByEventType 是一個 Map,key 為 eventType,value 為 CopyOnWriteArrayList,這個Map非常重要,後續還會用到它;
接下來,就是新增 newSubscription,它屬於 Subscription 類,裡面包含著 subscriber 和 subscriberMethod 等資訊,同時這裡有一個優先順序的判斷,說明它是按照優先順序新增的。優先順序越高,會插到在當前 List 靠前面的位置;
typesBySubscriber 這個類也是一個 Map,key 為 subscriber,value 為 subscribedEvents,即所有的 eventType 列表,這個類我找了一下,發現在 EventBus#isRegister()方法中有用到,應該是用來判斷這個 Subscriber 是否已被註冊過。然後將當前的 eventType新增到 subscribedEvents 中;
最後,判斷是否是 sticky。如果是sticky事件的話,到最後會呼叫 checkPostStickyEventToSubscription() 方法。
這裡其實就是將所有含 @Subscribe 註解的訂閱方法最終儲存在 subscriptionsByEventType中。
EventBus#checkPostStickyEventToSubscription():
private void checkPostStickyEventToSubscription(Subscription newSubscription, Object stickyEvent) {
if (stickyEvent != null) {
// If the subscriber is trying to abort the event, it will fail (event is not tracked in posting state)
// --> Strange corner case, which we don't take care of here.
postToSubscription(newSubscription, stickyEvent, isMainThread());
}
}
接下來,我們重點看 post() 和 postToSubscription() 方法。post 事件相當於把事件傳送出去,我們看看訂閱者是如何接收到事件的。
EventBus#post():
/** Posts the given event to the event bus. */
public void post(Object event) {
// 5.1
PostingThreadState postingState = currentPostingThreadState.get();
List <Object> eventQueue = postingState.eventQueue;
eventQueue.add(event);
// 5.2
if (!postingState.isPosting) {
postingState.isMainThread = isMainThread();
postingState.isPosting = true;
if (postingState.canceled) {
throw new EventBusException("Internal error. Abort state was not reset");
}
try {
while (!eventQueue.isEmpty()) {
postSingleEvent(eventQueue.remove(0), postingState);
}
} finally {
postingState.isPosting = false;
postingState.isMainThread = false;
}
}
}
註釋5.1 下程式碼段分析
currentPostingThreadState 是一個 ThreadLocal 型別的,裡面儲存了 PostingThreadState,而 PostingThreadState 中包含了一個 eventQueue 和其他一些標誌位;
然後把傳入的 event,儲存到了當前執行緒中的一個變數 PostingThreadState 的 eventQueue 中。
private final ThreadLocal <PostingThreadState> currentPostingThreadState = new ThreadLocal <PostingThreadState> () {
@Override
protected PostingThreadState initialValue() {
return new PostingThreadState();
}
};
/** For ThreadLocal, much faster to set (and get multiple values). */
final static class PostingThreadState {
final List <Object> eventQueue = new ArrayList<>();
boolean isPosting;
boolean isMainThread;
Subscription subscription;
Object event;
boolean canceled;
}
註釋5.2 下程式碼段分析
這裡涉及到兩個標誌位,第一個是 isMainThread,判斷是否為 UI 執行緒;第二個是 isPosting,作用是防止方法多次呼叫。
最後呼叫到 postSingleEvent() 方法
EventBus#postSingleEvent():
private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
Class<?> eventClass = event.getClass();
boolean subscriptionFound = false;
if (eventInheritance) {
List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
int countTypes = eventTypes.size();
for (int h =
相關推薦
從原始碼入手來學習EventBus 3事件匯流排機制
今日科技快訊3月28日訊息,二次元網站AcFun.com(A站)的本輪融資幾經波折仍未落定,這一
從另一個思路來學習安卓事件分發機制
從另一個思路來學習安卓事件分發機制
前言
事件分發機制是一個安卓老生常談的話題了,從前幾年的面試必問題到如今的本當成預設都會的基礎知識。關於這方面的部落格網上已經有很多很多了,有從原始碼分析的,有從實際出發開始分析的等等。面對這麼多的教程,小白可能一頭霧水不知道從哪裡看起,而且看完之後感覺啥也沒留下。那麼
Vue 2.0學習筆記:事件匯流排(EventBus)
許多現代JavaScript框架和庫的核心概念是能夠將資料和UI封裝在模組化、可重用的元件中。這對於開發人員可以在開發整個應用程式時避免使用編寫大量重複的程式碼。雖然這樣做非常有用,但也涉及到元件之間的資料通訊。在Vue中同樣有這樣的概念存在。通過前面一段時間的學習,Vue元件資料通訊常常會有父子元
從原始碼入手,一文帶你讀懂Spring AOP面向切面程式設計
基於這兩者的實現上,這次來探索下Spring的AOP原理。雖然AOP是基於Spring容器和動態代理,但不瞭解這兩者原理也絲毫不影響理解AOP的原理實現,因為大家起碼都會用。
AOP,Aspect Oriented Programming,面向切面程式設計。在很多
通過實際部署應用程式來學習Web 3.0:動手實踐(IPFS +以太坊)
“分散式網路”或“Web 3.0”現因其將給當今行業帶來的革命性變革已儼然成為流行語。但是我們中有多少人真正瞭解Web 3.0呢?
在本文中,我會對Web 3.0的顯著特點進行介紹。在獲得對Web 3.0的基本瞭解之後,我們將一起在IPFS上部署應用程式。該應用程式具有一份Solidit
Python:GUI之tkinter學習筆記3事件綁定(轉載自https://www.cnblogs.com/progor/p/8505599.html)
borde proto mes level 字符串 from .com 當前 控件 相關內容:
command
bind
protocol
首發時間:2018-03-04 19:26
command:
command是控件中的一個參數,如果使得comma
Python:GUI之tkinter學習筆記3事件繫結(轉載自https://www.cnblogs.com/progor/p/8505599.html) Python:GUI之tkinter學習筆記3事件繫結
Python:GUI之tkinter學習筆記3事件繫結
相關內容:
command
bind
protocol
首發時間:2018-03-04 19:26
command:
藍芽ble 從LED實驗來學習CC2541 IO口配置
帶著從頭到尾好好學習CC2540和CC2541的目的,從最基本的專案開始
本人QQ 330952038,歡迎交流學習
本文從最基本的LED實驗開始講起。 LED實驗主要通過控制IO口,實現LED的點亮和熄滅。
一、 硬體平臺
SmartRF 開發板,MC
從spring原始碼汲取營養:模仿spring事件釋出機制,解耦業務程式碼
前言
最近在專案中做了一項優化,對業務程式碼進行解耦。我們部門做的是警用系統,通俗的說,可理解為110報警。一條警情,會先後經過接警員、處警排程員、一線警員,警情是需要記錄每一步的日誌,是要可追溯的,比如報警人張小三在2019-12-02 00:02:01時間報警,接警員A在1分鐘後,將該警情記錄完成,並分派
[原始碼分析] 從原始碼入手看 Flink Watermark 之傳播過程
[原始碼分析] 從原始碼入手看 Flink Watermark 之傳播過程
0x00 摘要
本文將通過原始碼分析,帶領大家熟悉Flink Watermark 之傳播過程,順便也可以對Flink整體邏輯有一個大致把握。
0x01 總述
從靜態角度講,watermarks是實現流式計算的核心概念;從動態角度說,w
Alink漫談(二) : 從原始碼看機器學習平臺Alink設計和架構
# Alink漫談(二) : 從原始碼看機器學習平臺Alink設計和架構
[TOC]
## 0x00 摘要
Alink 是阿里巴巴基於實時計算引擎 Flink 研發的新一代機器學習演算法平臺,是業界首個同時支援批式演算法、流式演算法的機器學習平臺。本文是漫談系列的第二篇,將從原始碼入手,帶領大傢俱體剖析
從原始碼角度來分析執行緒池-ThreadPoolExecutor實現原理
作為一名Java開發工程師,想必效能問題是不可避免的。通常,在遇到效能瓶頸時第一時間肯定會想到利用快取來解決問題,然而快取雖好用,但也並非萬能,某些場景依然無法覆蓋。比如:需要實時、多次呼叫第三方API時,該場景快取則無法適用。
然多執行緒併發的方式則很好的解決了上述問題。
但若每次都在任務開始
用實驗方法學習View的事件傳遞機制
我寫了一個UI介面,最外層是OuterLinearLayout,內層是InnerLayout,最裡層是TargetButton,下面是實驗輸出的日誌:
2018-11-25 22:15:27.288 28703-28703/work.cloud.com.myappcloudwork D
從原始碼一次徹底理解Android的訊息機制
情景重現
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v
Hadoop學習筆記—3.Hadoop RPC機制的使用
一、RPC基礎概念
1.1 RPC的基礎概念
RPC,即Remote Procdure Call,中文名:遠端過程呼叫;
(1)它允許一臺計算機程式遠端呼叫另外一臺計算機的子程式,而不用去關心底層的網路通訊細節,對我們來說是透明的。因此,它經常用於分散式網路通訊中。
RPC協議假定某些傳輸
cocos2d-x學習筆記——EventDispatcher事件分發機制
EventDispatcher 事件分發機制先建立事件,註冊到事件管理中心 _eventDispatcher,通過釋出事件得到響應進行回撥,完成事件流。_eventDispatcher是Node的屬性,通過它管理當前節點(場景、層、精靈等)的所有事件的分發。但它
從原始碼角度分析Android系統的異常捕獲機制是如何執行的
我們在開發的時候經常會遇到各種異常,當程式遇到異常,便會將異常資訊拋到LogCat中,那這個過程是怎麼實現的呢?
我們以一個例子開始:
import android.app.Activity;
import android.os.Bundle;
public clas
Redis原始碼解析:13Redis中的事件驅動機制
Redis中,處理網路IO時,採用的是事件驅動機制。但它沒有使用libevent或者libev這樣的庫,而是自己實現了一個非常簡單明瞭的事件驅動庫ae_event,主要程式碼僅僅400行左右。
沒有選擇libevent或libev的原因大概在於,這些庫為了
原始碼分析:Android 的onTouch事件傳遞機制分析
當用戶觸控式螢幕幕的時候,最先接受到觸控事件的是Activity的dispatchTouchEvent().
我們就從這裡開始分析事件的分發
Activity原始碼
看下Activity的dispatchTouchEvent()原始碼。
Android6.0原始碼解讀之Activity點選事件分發機制
本篇博文是Android點選事件分發機制系列博文的第四篇,主要是從解讀Activity類的原始碼入手,根據原始碼理清Activity點選事件分發原理,並掌握Activity點選事件分法機制。特別宣告的是,本原始碼解讀是基於最新的Android6.0版本。