1. 程式人生 > >Android 框架煉成 教你如何寫元件間通訊框架EventBus

Android 框架煉成 教你如何寫元件間通訊框架EventBus

轉載請標明出處:

1、概述

首先我們回顧一下,這玩意就是在register時,掃描類中複合命名規範的方法,存到一個map,然後post的時候,查詢到匹配的方法,反射呼叫;好,那麼根據這一句話,我們就開始編寫框架之旅~~~

2、依然是原來的配方

1、ItemListFragment

package com.angeldevil.eventbusdemo;

import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;

import com.angeldevil.eventbusdemo.Event.ItemListEvent;
import com.zhy.eventbus.EventBus;

public class ItemListFragment extends ListFragment
{

	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		// Register
		EventBus.getInstatnce().register(this);
	}

	@Override
	public void onDestroy()
	{
		super.onDestroy();
		// Unregister
		EventBus.getInstatnce().unregister(this);
	}

	@Override
	public void onViewCreated(View view, Bundle savedInstanceState)
	{
		super.onViewCreated(view, savedInstanceState);
		// 開啟執行緒載入列表
		new Thread()
		{
			public void run()
			{
				try
				{
					Thread.sleep(2000); // 模擬延時
					// 釋出事件,在後臺執行緒發的事件
					EventBus.getInstatnce().post(new ItemListEvent(Item.ITEMS));
				} catch (InterruptedException e)
				{
					e.printStackTrace();
				}
			};
		}.start();
	}

	public void onEventUI(ItemListEvent event)
	{
		setListAdapter(new ArrayAdapter<Item>(getActivity(),
				android.R.layout.simple_list_item_activated_1,
				android.R.id.text1, event.getItems()));
	}

	@Override
	public void onListItemClick(ListView listView, View view, int position,
			long id)
	{
		super.onListItemClick(listView, view, position, id);
		EventBus.getInstatnce().post(getListView().getItemAtPosition(position));
	}

}

2、ItemDetailFragment

package com.angeldevil.eventbusdemo;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.zhy.eventbus.EventBus;

public class ItemDetailFragment extends Fragment
{

	private TextView tvDetail;

	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		// register
		EventBus.getInstatnce().register(this);
	}

	@Override
	public void onDestroy()
	{
		super.onDestroy();
		// Unregister
		EventBus.getInstatnce().unregister(this);
	}

	/** List點選時會發送些事件,接收到事件後更新詳情 */
	public void onEventUI(Item item)
	{
		if (item != null)
			tvDetail.setText(item.content);
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState)
	{
		View rootView = inflater.inflate(R.layout.fragment_item_detail,
				container, false);
		tvDetail = (TextView) rootView.findViewById(R.id.item_detail);
		return rootView;
	}
}

可以看到,我們在ItemListFragment裡面使用了:

EventBus.getInstatnce().post(new ItemListEvent(Item.ITEMS));去釋出了一個事件,然後更新了我們的列表;

點選Item的時候,使用EventBus.getInstatnce().post(getListView().getItemAtPosition(position));釋出了一個事件,更新了我們的ItemDetailFragment的列表;

效果:


效果圖和之前的一摸一樣~~~

但是請注意,現在我們用的是EventBus.getInstatnce();併發是EventBus.getDefault();並且看下包名import com.zhy.eventbus.EventBus;

我想你應該明白了,這是我們自己寫的類來實現的~~~~

好了,接下來就帶大家一起實現這個類~~

ps :以上程式碼和效果圖,完全是為了部落格的完整性,勿見怪~~

3、無中生有

1、getInstance

我們這裡為了方便,直接簡單粗暴的使用惡漢模式建立單例:

	private static EventBus eventBus = new EventBus();
	
	public static EventBus getInstatnce()
	{
		return eventBus;
	}
	
	private EventBus()
	{
		mHandler = new Handler(Looper.getMainLooper());
	}

然後在構造方法中初始化了一個mHandler,沒錯,它就是用來在處理在UI執行緒呼叫方法的。

接下來看register

2、register

/*
	 * 我們的強大的map,儲存我們的方法
	 */
	private static Map<Class, CopyOnWriteArrayList<SubscribeMethod>> mSubscribeMethodsByEventType = new HashMap<Class, CopyOnWriteArrayList<SubscribeMethod>>();

	public void register(Object subscriber)
	{

		Class clazz = subscriber.getClass();
		Method[] methods = clazz.getDeclaredMethods();

		CopyOnWriteArrayList<SubscribeMethod> subscribeMethods = null;
		/**
		 * 遍歷所有方法
		 */
		for (Method method : methods)
		{
			String methodName = method.getName();
			/**
			 * 判斷方法是否以onEvent的開頭
			 */
			if (methodName.startsWith("onEvent"))
			{
				SubscribeMethod subscribeMethod = null;
				// 方法命中提前在什麼執行緒執行。預設在UI執行緒
				String threadMode = methodName.substring("onEvent".length());
				ThreadMode mode = ThreadMode.UI;

				Class<?>[] parameterTypes = method.getParameterTypes();

				// 引數的個數為1
				if (parameterTypes.length == 1)
				{
					Class<?> eventType = parameterTypes[0];

					synchronized (this)
					{

						if (mSubscribeMethodsByEventType.containsKey(eventType))
						{
							subscribeMethods = mSubscribeMethodsByEventType
									.get(eventType);
						} else
						{
							subscribeMethods = new CopyOnWriteArrayList<SubscribeMethod>();
							mSubscribeMethodsByEventType.put(eventType,
									subscribeMethods);
						}
					}

					if (threadMode.equals("Async"))
					{
						mode = ThreadMode.Async;
					}
					// 提取出method,mode,方法所在類物件,存數的型別封裝為SubscribeMethod
					subscribeMethod = new SubscribeMethod(method, mode,
							subscriber);
					subscribeMethods.add(subscribeMethod);
				}
			}

		}
	}

	enum ThreadMode
	{
		UI, Async
	}

	class SubscribeMethod
	{
		Method method;
		ThreadMode threadMode;
		Object subscriber;

		public SubscribeMethod(Method method, ThreadMode threadMode,
				Object subscriber)
		{
			this.method = method;
			this.threadMode = threadMode;
			this.subscriber = subscriber;
		}

	}


可以看到我們使用了一個Map儲存所有的方法,key為引數的型別class;value為CopyOnWriteArrayList<SubscribeMethod>

這裡我們封裝了一個SubscribeMethod,這個裡面儲存了我們需要執行方法的所有引數,畢竟我們執行時,需要該方法,該方法所在的物件,以及在什麼執行緒執行;三個物件足以,當然也缺一不可了~~

register裡面,我們遍歷該類的所有方法,找到onEvent開頭的,封裝成SubscribeMethod,存在Map裡面,當然了,一個引數型別對應很多方法,所以value是個CopyOnWriteArrayList。

掃描完成,我們就完成了將方法的儲存。

還有一點,我們這裡預設在UI執行緒執行,如果方法是onEventAsync則認為在子執行緒執行,我們也只支援這兩種模式,簡化一點~

3、post

private static ThreadLocal<PostingThread> mPostingThread = new ThreadLocal<PostingThread>()
	{
		@Override
		public PostingThread get()
		{
			return new PostingThread();
		}
	};
	
	

	public void post(Object eventTypeInstance)
	{
		//拿到該執行緒中的PostingThread物件
		PostingThread postingThread = mPostingThread.get();
		postingThread.isMainThread = Looper.getMainLooper() == Looper
				.myLooper();
		//將事件加入事件佇列
		List<Object> eventQueue = postingThread.mEventQueue;
		eventQueue.add(eventTypeInstance);
		//防止多次呼叫
		if (postingThread.isPosting)
		{
			return;
		}
		postingThread.isPosting = true;
		//取出所有事件進行呼叫
		while (!eventQueue.isEmpty())
		{
			Object eventType = eventQueue.remove(0);
			postEvent(eventType, postingThread);
		}
		postingThread.isPosting = false;

	}

我們這裡學習了原始碼,也搞了個當前執行緒中的變數,儲存了一個事件佇列以及事件的狀態;

class PostingThread
{
	List<Object> mEventQueue = new ArrayList<Object>();
	boolean isMainThread;
	boolean isPosting;
}

最終釋出的事件先加入到事件佇列,然後再取出來呼叫postEvent

private void postEvent(final Object eventType, PostingThread postingThread)
	{
		CopyOnWriteArrayList<SubscribeMethod> subscribeMethods = null;
		synchronized (this)
		{
			subscribeMethods = mSubscribeMethodsByEventType.get(eventType
					.getClass());
		}

		for (final SubscribeMethod subscribeMethod : subscribeMethods)
		{

			if (subscribeMethod.threadMode == ThreadMode.UI)
			{
				if (postingThread.isMainThread)
				{
					invokeMethod(eventType, subscribeMethod);
				} else
				{
					mHandler.post(new Runnable()
					{
						@Override
						public void run()
						{
							invokeMethod(eventType, subscribeMethod);
						}
					});
				}
			} else
			{
				new AsyncTask<Void, Void, Void>()
				{

					@Override
					protected Void doInBackground(Void... params)
					{
						invokeMethod(eventType, subscribeMethod);
						return null;
					}
				};
				
			}

		}

	}

postEvent也很簡單,直接根據引數型別,去map改到該方法,根據其threadMode,如果在UI執行緒,則判斷當前執行緒,如果是UI執行緒,直接呼叫,否則通過handler執行;

如果非UI執行緒,這裡我們直接開啟了一個Thread去執行;

invokeMethod很簡單,就是反射呼叫方法了~

private void invokeMethod(Object eventType, SubscribeMethod subscribeMethod)
	{
		try
		{
			subscribeMethod.method
					.invoke(subscribeMethod.subscriber, eventType);
		} catch (Exception e)
		{
			e.printStackTrace();
		}
	}

4、unregister

public void unregister(Object subscriber)
	{
		Class clazz = subscriber.getClass();
		Method[] methods = clazz.getDeclaredMethods();

		List<SubscribeMethod> subscribeMethods = null;

		for (Method method : methods)
		{
			String methodName = method.getName();

			if (methodName.startsWith("onEvent"))
			{
				Class<?>[] parameterTypes = method.getParameterTypes();
				if (parameterTypes.length == 1)
				{
					synchronized (this)
					{
						mSubscribeMethodsByEventType.remove(parameterTypes[0]);
					}
				}
			}
		}

	}

unregister時,由於我們沒有存任何的輔助狀態,我們只能再去遍歷了方法了~~不過通過這個,也能反應出EventBus內部好幾個Map的作用了~~

並且,我們也不支援一些狀態的查詢,還是因為我們沒有存一些輔助狀態,例如isRegister等等。

到此,我們的EventBus就寫好了,100多行程式碼,肯定沒有EventBus健壯,主要目的還是學習人家的思想,經過自己寫了這麼個類,我相信對於EventBus的理解就更深刻了~面試的時候,恨不得拿只筆寫給面試官看,哈哈~~

5、EventBus最佳實踐

前面的文章,很多朋友問,如果我多個方法引數都一樣,豈不是post一個此引數,會多個方法呼叫;而此時我想呼叫指定的方法怎麼辦?

還有,專案中會有很多地方去接收List引數,而List<T>中的泛型是不一致的,所以也可能post(List)時,會呼叫很多方法,造成出錯。

的確,上述,不加處理肯定會出現;

但是,推薦大家在使用EventBus的時候,建立一個事件類,把你的每一個引數(或者可能發生衝突的引數),封裝成一個類:

例如:

public class Event
{
	public static class UserListEvent
	{
		public List<User> users ;
	}
	public static class ItemListEvent
	{
		public List<Item> items;
	}

}

這樣的話,就不會發生什麼呼叫衝突了~~

建了一個QQ群,方便大家交流。群號:55032675

----------------------------------------------------------------------------------------------------------

博主部分視訊已經上線,如果你不喜歡枯燥的文字,請猛戳(初錄,期待您的支援):