Android主流IOC框架淺析(Java註解反射原理)
TextView mTextView;
mTextView=(TextView) findViewById(R.id.mTextView);
mTextView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
}
});
作為一名Android程式設計師,對於上面這種機械化的程式碼你一定寫到想吐了,或許多數時候你只是copy ,paste,然後再改一改,完了你可能又會覺得這種程式碼毫無營養,寫得實在沒勁。俗話說:“不會偷懶的程式設計師不是好程式設計師”,今天我們就來探討下如何偷懶。
到這裡可能你已經知道我要說的是什麼了,是的,我要說的就是Android中的IOC框架,這類框架中比較早的有:Afinal,Xutils,目前開發者中呼聲比較高的有Android Annotations,ButterKnife,Dagger,RoboGuice等。我在這裡就簡單介紹下比較有代表性的Android Annotations和ButterKnife。
什麼是IOC?
Inversion of Control,英文縮寫為IOC,字面翻譯:控制反轉。什麼意思呢?就是一個類裡面需要用到很多個成員變數,傳統的寫法,你要用這些成員變數,那麼你就new 出來用唄!IOC的原則是:NO,我們不要new,這樣耦合度太高,你配置個xml檔案,裡面標明哪個類,裡面用了哪些成員變數,等待載入這個類的時候,我幫你注入(new)進去;當然了,你又會覺得,寫個配置檔案,臥槽,這多麻煩。於是乎,又出現了另一種方案,得~你嫌配置檔案麻煩,那用註解唄~在你需要注入的成員變數上面加個註解,例如:@Inject,這樣就行了,你總不能說這麼個單詞麻煩吧。當然了,有了配置檔案和註解,那麼怎麼注入呢?其實就是把字串類路徑變成類麼,這個時候需要用到反射。
什麼是反射?
首先JAVA語言並不是一種動態程式語言,而為了使語言更靈活,JAVA引入了反射機制。 JAVA反射機制是在執行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意一個方法;這種動態獲取的資訊以及動態呼叫物件的方法的功能稱為java語言的反射機制。Java反射機制主要提供了以下功能: 在執行時判斷任意一個物件所屬的類;在執行時構造任意一個類的物件;在執行時判斷任意一個類所具有的成員變數和方法;在執行時呼叫任意一個物件的方法;生成動態代理。
什麼是註解?
JAVA1.5之後引入的註解和反射,註解的實現依賴於反射。JAVA中的註解是一種繼承自介面java.lang.annotation.Annotation的特殊介面。那麼介面怎麼能夠設定屬性呢?簡單來說就是JAVA通過動態代理的方式為你生成了一個實現了介面Annotation的例項,然後對該代理例項的屬性賦值,這樣就可以在程式執行時(如果將註解設定為執行時可見的話)通過反射獲取到註解的配置資訊。說的通俗一點,註解相當於一種標記,在程式中加了註解就等於為程式打上了某種標記。程式可以利用JAVA的反射機制來了解你的類及各種元素上有無何種標記,針對不同的標記,就去做相應的事件。標記可以加在包,類,方法,方法的引數以及成員變數上。
以上算是背景介紹吧,也正是基於以上需求和實現原理,一大波Android IOC框架應運而生。我們先來看第一個:Android Annotations。我們先比較下常規寫法和Annotations寫法的程式碼。
常規寫法
先隨便來一段吧~~
public class MainActivity extends Activity {
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.text);
textView.setText("test");
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
Intent intent = new Intent(this, ChildActivity.class);
startActivity(intent);
}
});
}
}
Android Annotations寫法
@EActivity(R.layout.activity_main)
public class MainActivity extends Activity {
@ViewById(R.id.text)
TextView textView;
@AfterViews
public void init() {
textView.setText("annotations test");
}
@Click(R.id.text)
void buttonClick() {
Intent intent = new Intent(this,ChildActivity_.class);
startActivity(intent);
}
}
就是這麼簡單
Android Annotations就這麼寫?可能你會問,oncreat()去哪了,這可是執行入口,你又會擔心setContentView()呢,佈局怎麼載入?看上面的程式碼第一行@EActivity(R.layout.activity_main),就這一句全搞定,同時也不需要搞一大堆findViewById,不需要搞一大推setOnClickListener。Android Annotations能幹的事可遠不止這些,Http請求,開啟執行緒,事件繫結等等都可以通過一個註解標記搞定。
其他語法舉例
色值 @ColorRes
@ColorRes(R.color.backgroundColor)
int someColor;
@ColorRes
int backgroundColor;
動畫 @AnimationRes
@AnimationRes(R.anim.fadein)
XmlResourceParser xmlResAnim;
@AnimationRes
Animation fadein;
自定義View @EViewGroup
@EViewGroup(R.layout.title_with_subtitle)
public class TitleWithSubtitle extends RelativeLayout {
@ViewById
protected TextView title, subtitle;
public TitleWithSubtitle(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setTexts(String titleText, String subTitleText) {
title.setText(titleText);
subtitle.setText(subTitleText);
}
}
HttpClient @HttpsClient
@HttpsClient
HttpClient httpsClient;
UiThread @UiThread
void myMethod() {
doInUiThread("hello", 100);
}
@UiThread
void doInUiThread(String aParam, long anotherParam) {
[...]
}
怎麼實現的?
Android Annotations的實現原理其實很簡單。它會使用標準的Java註解處理工具自動新增一個額外的編譯步驟生成的原始碼。其實就是生成一個原有類的子類,這個子類才是真正執行用的類。例如上面程式碼使用@EActivity註解的MainActivity,將生成這個MainActivity的一個子類,它的名字是“MainActivity_”。該子類過載一些方法(例如onCreate()),通過委託方式呼叫了activity的相關方法。所以這裡有個大坑,所有Activity的相關操作都要操作其子類,例如 AndroidManifest.xml中類名要寫成android:name=".MainActivity_",開啟MainActivity要寫成 startActivity(new Intent(this, MainActivity_.class));下面是上文中AndroidAnnotations方式寫法的MainActivity的子類MainActivity_,我貼出來大家隨便感受下。
public final class MainActivity_
extends MainActivity
implements HasViews, OnViewChangedListener
{
private final OnViewChangedNotifier onViewChangedNotifier_ = new OnViewChangedNotifier();
@Override
public void onCreate(Bundle savedInstanceState) {
OnViewChangedNotifier previousNotifier = OnViewChangedNotifier.replaceNotifier(onViewChangedNotifier_);
init_(savedInstanceState);
super.onCreate(savedInstanceState);
OnViewChangedNotifier.replaceNotifier(previousNotifier);
setContentView(layout.activity_main);
}
private void init_(Bundle savedInstanceState) {
OnViewChangedNotifier.registerOnViewChangedListener(this);
}
@Override
public void setContentView(int layoutResID) {
super.setContentView(layoutResID);
onViewChangedNotifier_.notifyViewChanged(this);
}
@Override
public void setContentView(View view, LayoutParams params) {
super.setContentView(view, params);
onViewChangedNotifier_.notifyViewChanged(this);
}
@Override
public void setContentView(View view) {
super.setContentView(view);
onViewChangedNotifier_.notifyViewChanged(this);
}
public static MainActivity_.IntentBuilder_ intent(Context context) {
return new MainActivity_.IntentBuilder_(context);
}
public static MainActivity_.IntentBuilder_ intent(Fragment supportFragment) {
return new MainActivity_.IntentBuilder_(supportFragment);
}
@Override
public void onViewChanged(HasViews hasViews) {
textView = ((TextView) hasViews.findViewById(id.text));
if (textView!= null) {
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
MainActivity_.this.buttonClick();
}
}
);
}
init();
}
public static class IntentBuilder_
extends ActivityIntentBuilder<MainActivity_.IntentBuilder_>
{
private Fragment fragmentSupport_;
public IntentBuilder_(Context context) {
super(context, MainActivity_.class);
}
public IntentBuilder_(Fragment fragment) {
super(fragment.getActivity(), MainActivity_.class);
fragmentSupport_ = fragment;
}
@Override
public void startForResult(int requestCode) {
if (fragmentSupport_!= null) {
fragmentSupport_.startActivityForResult(intent, requestCode);
} else {
if (context instanceof Activity) {
Activity activity = ((Activity) context);
ActivityCompat.startActivityForResult(activity, intent, requestCode, lastOptions);
} else {
context.startActivity(intent);
}
}
}
}
}
ButterKnife
再說說ButterKnife吧,其實原理和Android Annotations差不多,只是一些寫法和細節上有略微差別,最主要一點是沒有Android Annotations的坑,Android Studio上有個ButterKnife的外掛,這個外掛提供的炫酷技能幾乎是一鍵搞定findViewById和setOnClickListener,基於此ButterKnife被很多開發者所推崇,我們還是簡單對比下程式碼吧。
常規寫法
public class MainActivity extends Activity {
TextView textView;
ListView listView;
ListViewAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.text);
listView = (ListView) findViewById(R.id.ListView);
textView.setText("test");
adapter = new ListViewAdapter(MainActivity.this);
listView.setAdapter(adapter);
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
text.setText("你點選了按鈕");
}
});
}
class ListViewAdapter extends BaseAdapter {
private Context mContext;
public ListViewAdapter(Context context) {
mContext = context;
}
@Override
public int getCount() {
// TODO Auto-generated method stub
return 1000;
}
@Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return position;
}
@Override
public long getItemId(int position) {
// TODO Auto-generated method stub
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
holder = new ViewHolder();
LayoutInflater layoutInflater = ((Activity) mContext)
.getLayoutInflater();
convertView = layoutInflater.inflate(R.layout.list_item,
parent, false);
holder.imageview = (ImageView) convertView
.findViewById(R.id.headshow);
holder.textview0 = (TextView) convertView
.findViewById(R.id.name);
holder.textview1 = (TextView) convertView
.findViewById(R.id.text);
convertView.setTag(convertView);
} else {
convertView = (View) convertView.getTag();
}
holder.textview0.setText("star");
holder.textview1.setText("test");
return convertView;
}
}
static class ViewHolder {
ImageView imageview;
TextView textview0;
TextView textview1;
}
}
ButterKnife寫法
public class MainActivity extends Activity {
@InjectView(R.id.text)
TextView text;
@InjectView(R.id.ListView)
ListView listView;
ListViewAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.inject(this);
adapter = new ListViewAdapter(MainActivity.this);
listView.setAdapter(adapter);
text.setText("ButterKnife test");
}
@OnClick(R.id.text)
void onClick() {
text.setText("你點選了按鈕");
}
class ListViewAdapter extends BaseAdapter {
private Context mContext;
public ListViewAdapter(Context context) {
mContext = context;
}
@Override
public int getCount() {
// TODO Auto-generated method stub
return 1000;
}
@Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return position;
}
@Override
public long getItemId(int position) {
// TODO Auto-generated method stub
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
LayoutInflater layoutInflater = ((Activity) mContext)
.getLayoutInflater();
convertView = layoutInflater.inflate(
R.layout.list_item, parent, false);
holder = new ViewHolder(convertView);
convertView.setTag(convertView);
} else {
convertView = (View) convertView.getTag();
}
holder.textview0.setText("butterknife");
holder.textview1.setText("test");
return convertView;
}
}
static class ViewHolder {
@InjectView(R.id.headshow)
ImageView imageview;
@InjectView(R.id.name)
TextView textview0;
@InjectView(R.id.text)
TextView textview1;
public ViewHolder(View view) {
ButterKnife.inject(this, view);
}
}
}
整體寫法與Android Annotations類似,實現原理上ButterKnife的實現也是在編譯的過程中生成了另外一個類來幫我們完成一些基本操作,以上面的程式碼為例,生成了一個名為MainActivity$$ViewInjector的類,這裡我就不再貼程式碼了。但與Android Annotations所不同的是,我們在程式碼中操作的還是MainActivity,而並不是MainActivity$$ViewInjector。
ButterKnife其他語法列舉
資源注入
class ExampleActivity extends Activity {
@BindString(R.string.title) String title;
@BindDrawable(R.drawable.graphic) Drawable graphic;
@BindColor(R.color.red) int red;
@BindDimen(R.dimen.spacer) Float spacer;
// ...
}
Fragment注入
public class FancyFragment extends Fragment {
@InjectView(R.id.button1) Button button1;
@InjectView(R.id.button2) Button button2;
@Override View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fancy_fragment, container, false);
ButterKnife.inject(this, view);
// TODO Use "injected" views...
return view;
}
}
回撥函式注入
// 帶有 Button 引數
@OnClick(R.id.submit)
public void sayHi(Button button) {
button.setText("Hello!");
}
// 不帶引數
@OnClick(R.id.submit)
public void submit() {
// TODO submit data to server...
}
// 同時注入多個 View 事件
@OnClick({ R.id.door1, R.id.door2, R.id.door3 })
public void pickDoor(DoorView door) {
if (door.hasPrizeBehind()) {
Toast.makeText(this, "You win!", LENGTH_SHORT).show();
} else {
Toast.makeText(this, "Try again", LENGTH_SHORT).show();
}
}
效能如何
關於IOC框架的基本寫法和實現原理,通過上面兩個例子,相信大家都已經有所瞭解。但是前面已經說了IOC的註解機制是依賴JAVA的反射,可能很多開發者都會嗤之以鼻:反射會影響效能。在早期的JAVA語言中反射是會帶來不小的效能消耗,而隨著語言自身的進步和完善,到了現在情況有所好轉。但我們移動裝置的效能,不比後臺伺服器擁有充足的記憶體和運算能力。當大量的使用註解的時候,會不會對APP造成什麼不良的影響,會不會影響到APP的執行效能?在這裡先明確的宣告,上述框架不會給APP帶來任何副作用,相反它強大易用的api能為你帶來前所未有的程式設計體驗。
上述框架的實現,都是通過使用jdk 1.6引入的Java Annotation Processing Tool,在編譯器中加了一層額外的自動編譯步驟,用來生成基於你原始碼的程式碼。執行期執行的其實就是這個二次編譯的程式碼,實際上反射註解這一過程是在編譯期完成的,而並不會影響Runtime。所以上述IOC框架並不會影響到APP得效能。簡單的做了幾組測試,分別用Android Annotations,ButterKnife,以及常規寫法寫了同樣實現的程式碼,然後列印程式碼執行時間。每組大概跑200次,然後計算程式碼執行的平均耗時,發現三組資料基本是在同一個水平上,並不能判斷孰優孰劣。
IDE整合方法
不管是Eclipse還是Android Studio都可以很方便的整合上述框架,而且資源包很小,具體方法大家網上找一找,我就不再列出具體步驟了。從網上偷了張ButterKnife的外掛技能圖,大家隨便感受下。
20140122235713140.gif
既然此類框架如此炫酷高效,那麼我們是否應該大肆推崇,廣泛採用呢。這個具體還是根據自己的專案實際情況來定吧,據我小範圍瞭解,目前部分大廠出品的部分應用並未採用此類框架,最後我們還是來看看此類框架的優缺點吧。
優點
1:提高開發效率
2:減少程式碼量
缺點
1:程式碼可讀性差
2:增加新人學習成本
3:加速觸及65535方法數問
你用或者不用,框架就在那裡,不悲不喜!
(如有刊物,歡迎指正)