Android从程序员到架构师之路3
生活随笔
收集整理的這篇文章主要介紹了
Android从程序员到架构师之路3
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
本文學習自高煥堂老師的Android從程序員到架構師之路系列教學視頻
40 - 認識線程(Thread)模式a
1. 線程(Thread)概念
所謂線程(Thread) 是指一串連續的執行動作,以達成一項目的。現代的電腦內部都有數串連續性的動作同時在進行。也就是有多條線程并行地(Concurrently)執行。在電腦中,若電腦擁有多顆CPU,則每顆CPU 可各照顧一個線程,于是可多個線程同時間進行。若只有單一CPU,則此CPU可同時(Concurrently)照顧數個線程。無論是多CPU或單一CPU的電腦,多條線程并行地執行,都可增加執行效率。像Java、C++等現代的電腦語言都能讓程序師們能夠易于創建多條線程減化GUI 動畫的設計工作,也可增加其執行效率。例如,當您想一邊看動畫,一邊聽音樂時,計算機能同時產生兩個線程──“秀動畫”及“播音樂”。甚至可產生另一條線程來為您做特殊服務,如讓您可選擇動畫及音樂。多條線程能并行地執行同一個類別,或者是不同的類別。在Android平臺里也不例外,無論是在Java層或是C++層,都常常見到多條線程并行的情形。Android采取Java的Thread框架,來協助建立多條線程並行的環境。在Java里,大家已經習慣撰寫一個類別來支持Runnable接口,再搭配Thread基類就能順利誕生一個新線程來執行該類別里的run()函數了。2. Java的線程框架
Java提供一個Thread基類(Super Class)來支持多線程功能。這個基類協助誕生(小)線程,以及管理(小)線程的進行,讓電腦系統更容易取得程序師的指示,然后安排CPU 來運作線程里的指令。 例如,線程所欲達成的任務(Task)是程序師的事,所以程序師應填寫線程里的指令,來表達其指示。為配合此部份的運作,Java提供了Runnable接口,其定義了一個run()函數。 于是,程序師可撰寫一個應用類別(Application Class)來實作(Implement)此界面,并且把線程的任務寫在應用類別的run()函數里,如此即可讓(小)線程來執行run()函數里的任務了。這是幾乎每一位Java開發者都常用的多線程(Multi-thread)機制,只是許多人都會用它,卻不曾認識它的真實身影:就是一個幕后的框架。由于Android應用程序開發也采用Java語言,所這個Thread框架也成為Android大框架里的一個必備元素。基于這個框架里的Thread基類和Runnable接口,你就可以撰寫應用類別,來實作run()函數了,如下圖: 于此圖里,框架的Thread基類會先誕生一個小線程,然后該小線程透過Runnable接口,調用(或執行)了Task類別的run()函數。 例如,請看一個Java程序:// Ex01-01.java class Task implements Runnable {public void run() {int sum = 0;for (int i = 0; i <= 100; i++)sum += i;System.out.println("Result: " + sum);} }public class JMain {public static void main(String[] args) {Thread t = new Thread(new Task());t.start();System.out.println("Waiting...");} }此時,main()先誕生一個Task類的對象,并且誕生一個Thread基礎的對象。接著,執行到下一個指令:t.start(); 此時,main()就調用Thread的start()函數;這start()就產生一個小線程去執行run()函數。如下圖:
41 - 認識線程(Thread)模式b
3. 認識Android的主線程(又稱UI線程)
UI線程的責任:迅速處理UI事件 在Android里,關照UI畫面的事件(Event)是UI線程的重要職責,而且是它的專屬職責,其它子線程并不可以插手存取UI畫面上的對象(如TextView)呢!由于Android希望UI線程能夠給予用戶的要求做快速的反應。如果UI 線程花費太多時間做幕后的事情,而在UI事件發生之后,讓用戶等待超過5秒鐘而未處理的話,Android就會向用戶道歉。 // ac01.java// ……..public class ac01 extends Activity implements OnClickListener {public TextView tv;private Button btn, btn2, btn3;public void onCreate(Bundle icicle) {super.onCreate(icicle);LinearLayout layout = new LinearLayout(this);layout.setOrientation(LinearLayout.VERTICAL);btn = new Button(this); btn.setId(101);btn.setBackgroundResource(R.drawable.heart);btn.setText("Block UI thread"); btn.setOnClickListener(this);LinearLayout.LayoutParams param = new LinearLayout.LayoutParams(150,50); param.topMargin = 10;layout.addView(btn, param);btn2 = new Button(this); btn2.setId(102);btn2.setBackgroundResource(R.drawable.heart);btn2.setText("Show"); btn2.setOnClickListener(this);layout.addView(btn2, param); btn3 = new Button(this); btn3.setId(103);btn3.setBackgroundResource(R.drawable.heart);btn3.setText("Exit"); btn3.setOnClickListener(this);layout.addView(btn3, param); tv = new TextView(this);tv.setTextColor(Color.WHITE); tv.setText("");LinearLayout.LayoutParams param2 = new LinearLayout.LayoutParams(150, 60); param2.topMargin = 10;layout.addView(tv, param2);setContentView(layout);setTitle("please press <Block...> & <Show> ");tv.setText("then wait for 5 min...");}public void onClick(View v) {switch(v.getId()){case 101:try { Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}break;case 102: break;case 103: finish(); break; } }} 連續按下<Block UI thread>和<Show>按鈕,然後等待5秒鐘,就會出現剛才所說的道歉提示主線程可以誕生多個子線程來分擔其工作,尤其是比較冗長費時的幕后服務工作,例如播放動畫的背景音樂、或從網絡下載影片等。于是,主線程就能專心于處理UI畫面的事件了。再如,當你開發一個游戲程序時,如果你希望游戲循環不受UI事件的干擾,或希望游戲循環(GameLoop)不要阻塞住UI線程的話,就不適合拿UI線程去跑游戲循環了。42 - 認識線程(Thread)模式c
UI線程的誕生
當我們啟動某一支AP時,Android就會誕生新進程(Process),并且將該AP程序加載這新誕生的進程里。每個進程在其誕生時刻,都會誕生一個主線程,又稱為UI線程 在進程誕生時刻,除了誕生主線程之外,還會替主線程誕生它專用的Message、Queue和Looper。如下圖所示: 這個Main Looper就是讓主線程沒事時就來執行Looper,確保主線程永遠活著而不會死掉;在執行Looper時,會持續觀察它的Message Queue是否有新的信息進來;如果有新信息進來的話,主線程就會盡快去處理(響應)它。在Android環境里,一個應用程序常包含有許多個類別,這些類別可以分布在不同進程里執行,或擠在一個進程里執行。例如有一個應用程序的AndroidManifest.xml文件內容如下: // AndroidManifest.xml// ………<activity android:name=".FirstActivity" android:label="@string/app_name"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter> </activity><activity android:name=".LoadActivity"><intent-filter><category android:name="android.intent.category.DEFAULT" /></intent-filter> </activity><service android:name=".LoadService" android:process=":remote"><intent-filter><action android:name="com.misoo.pkm.REMOTE_SERVICE" /></intent-filter> </service></application></manifest>Android依據這個文件而將各類別布署于兩個進程里執行,如圖: 其中,FirstActivity和LoadActivity兩個類別會加載預設的進程里。而LoadService則會加載于名為“remote”的獨立進程里。 于是,由進程#1的主線程去執行FirstActivity和LoadActivity的onCreate()等函數。而由進程#2的主線程去執行LoadService的onCreate()等函數。
練習:綁定(Bind)遠程的Service
43 - 認識線程(Thread)模式d
4. 細說主線程(UI線程)的角色
近程通信
在Android里,無論組件在那一個進程里執行,于預設情形下,他們都是由該進程里的主線程來負責執行之。 例如下述的范例,由一個Activity啟動一個Service,兩者都在同一個進程里執行。此時,兩者都是由主線程負責執行的。如下圖所示: // ac01.java //…… public class ac01 extends Activity implements OnClickListener {private Button btn, btn2;public void onCreate(Bundle icicle) {super.onCreate(icicle);LinearLayout layout = new LinearLayout(this);layout.setOrientation(LinearLayout.VERTICAL);btn = new Button(this); btn.setId(101);btn.setText("run service");btn.setBackgroundResource(R.drawable.heart);btn.setOnClickListener(this);LinearLayout.LayoutParams param= new LinearLayout.LayoutParams(135, 50);param.topMargin = 10; layout.addView(btn, param);btn2 = new Button(this); btn2.setId(102);btn2.setText("Exit");btn2.setBackgroundResource(R.drawable.heart);btn2.setOnClickListener(this);layout.addView(btn2, param);setContentView(layout);//---------------------------------------Thread.currentThread().setName(Thread.currentThread().getName()+"-ac01");}public void onClick(View v) {switch (v.getId()) {case 101:this.startService(new Intent(this, myService.class));break;case 102:finish(); break;}} }// myService.java //…….. public class myService extends Service {@Override public void onCreate(){Thread.currentThread().setName(Thread.currentThread().getName() + "-myService");Toast.makeText(this, Thread.currentThread().getName(),Toast.LENGTH_SHORT).show();}@Override public IBinder onBind(Intent intent){ return null; } }主線程先執行ac01的onCreate()函數,然后,繼續執行myService的onCreate()函數。于是,輸出了主線程的執行軌跡紀錄除了上述的Activity和Service之外,還有BroadcastReceiver也是一樣,是由主線程來執行的。例如,由一個Activity啟動一個BroadcastReceiver,兩者都在同一個進程里執行。此時,兩者都是由主線程負責執行的。如下圖所示 // ac01.java // ……. public class ac01 extends Activity implements OnClickListener {//…….public void onCreate(Bundle icicle) {//………Thread.currentThread().setName(Thread.currentThread().getName()+"-ac01");}public void onClick(View v) {switch (v.getId()) {case 101:Intent in = new Intent(MY_EVENT);this.sendBroadcast(in); break;case 102: finish(); break;}}} }// myReceiver.java //…….. public class myReceiver extends BroadcastReceiver {@Override public void onReceive(Context context, Intent intent) {Thread.currentThread().setName(Thread.currentThread().getName() + "-myReceiver");Toast.makeText(context, Thread.currentThread().getName(),Toast.LENGTH_SHORT).show();} }主線程先執行myActivity的onCreate()函數,之后繼續執行myReceiver的onReceive()函數。于是輸出了主線程執行的軌跡紀錄:遠程通信
如果Activity、Service和BroadcastReceiver三者并不是在同一個進程里執行時,它們之間的通訊就是跨進程通訊(IPC)了。 請先看個范例,它由一個Activity啟動一個遠距的Service,兩者分別在不同的進程里執行,如下圖所示: 當Activity與Service(或BroadcastReceiver)之間采用IPC通訊時,意味著兩者分別在不同的進程里執行。此時,于預設情形下,Activity、BroadcastReceiver或Service都是由其所屬進程里的主線程負責執行之 Android核心的Binder System從”remote”進程的線程池里,啟動一個線程(名為”Binder Thread #1”)來執行myBinder的onTransact()函數。 ? 依據Binder System的同步(Synchronization)的機制,主線程會等待Binder Thread #1線程執行完畢,才會繼續執行下去。44 - 認識線程(Thread)模式e
5.線程之間的通信架構
認識Looper與Handler對象
當主線程誕生時,就會去執行一個代碼循環(Looper),以便持續監視它的信息隊列(Message Queue簡稱MQ)。當UI事件發生了,通常會立即丟一個信息(Message)到MQ,此時主線程就立即從MQ里面取出該信息,并且處理之。例如,用戶在UI畫面上按下一個Button按鈕時,UI事件發生了,就會丟一些信息到MQ里,其中包括onClick信息,于是,主線程會及時從MQ里取出onClick信息,然后調用Activity的onClick()函數去處理之。 處理完畢之后,主線程又返回去繼續執行信息循環,繼續監視它的MQ,一直循環下去,直到主線程的生命周期的終了。 通常是進程被刪除時,主線程才會被刪除Android里有一個Looper類別,其對象里含有一個信息循環(Message Loop)。也就是說,一個主線程有它自己專屬的Looper對象,此線程誕生時,就會執行此對象里的信息循環。此外,一個主線程還會有其專屬的MQ信息結構。如下圖所示: 由于主線程會持續監視MQ的動態,所以在程序的任何函數,只要將信息(以Message類別的對象表示之)丟入主線程的MQ里,就能與主線程溝通了。 在Android里,也定義了一個Handler類別,在程序的任何函數里,可以誕生Handler對象來將Message對象丟入MQ里,而與主線程進行溝通。在Android的預設情況下,主線程誕生時,就會擁有自己的Looper對象和MQ(即Message Queue)數據結構。 然而,主線程誕生子線程時,于預設情形下,子線程并不具有自己的Looper對象和MQ。由于沒有Looper對象,就沒有信息回圈(Message Loop),一旦工作完畢了,此子線程就結束了。既然沒有Looper對象也沒有MQ,也就不能接受外來的Message對象了。則別的線程就無法透過MQ來傳遞信息給它了。 那么,如果別的線程(如主線程)需要與子線程通訊時,該如何呢? 答案是:替它誕生一個Looper對象和一個MQ就行了。主線程丟信息給自己Handler是Android框架所提供的基類,用來協助將信息丟到線程的MQ里。 茲撰寫個范例程序Rx01,來將信息丟到主線程的MQ里,如下:// ac01.java //…….. public class ac01 extends Activity implements OnClickListener {private Handler h;public void onCreate(Bundle icicle) {//……..h = new Handler(){public void handleMessage(Message msg) {setTitle((String)msg.obj);}}; }public void onClick(View v) {switch (v.getId()) {case 101:h.removeMessages(0);Message m = h.obtainMessage(1, 1, 1, "this is my message.");h.sendMessage(m); // 將Message送入MQ里break;case 102: finish(); break;}} }當主線程執行到onCreate()函數里的指令: h = new Handler(){// ……… } 就誕生一個Handler對象,可透過它來把信息丟到MQ里。 當執行到onClick()函數里的指令: //……………… h.removeMessages(0); Message m = h.obtainMessage(1, 1, 1, "this is my message."); h.sendMessage(m); 就將Message對象送入MQ里。當主線程返回到信息回圈時,看到MQ里有個Message對象,就取出來,并執行handleMessage()函數,將Message對象里所含的字符串顯示于畫面上。子線程丟信息給主線程子線程也可以誕生Handler對象來將Message對象丟到主線程的MQ里,又能與主線程通訊了。茲撰寫個范例程序Rx02 如下:// ac01.java // ………. public class ac01 extends Activity implements OnClickListener {private Handler h;private Timer timer = new Timer();private int k=0;public void onCreate(Bundle icicle) {super.onCreate(icicle);//………h = new Handler(){public void handleMessage(Message msg) {setTitle((String)msg.obj);}}; }public void onClick(View v) {switch (v.getId()) {case 101:TimerTask task = new TimerTask(){@Override public void run() {h.removeMessages(0);Message m = h.obtainMessage(1, 1, 1,Thread.currentThread().getName() + " : "+String.valueOf(k++));h.sendMessage(m);}};timer.schedule(task, 500, 1500); break;case 102:finish(); break;}} }就啟動一個Timer的線程,名字叫:”Timer-0”;然后,由它來定時重復執行TimerTask::run()函數,就不斷將Message對象丟到主線程的MQ里。此時主線程會持續處理MQ里的Message對象,將其內的字符串顯示于畫面上。于是,子執行透過Handler對象而將信息丟到主線程的MQ,進而成功地將信息顯示于畫面上。替子線程誕生Looper與MQ如果別的線程(如主線程)需要與子線程通訊時,該如何呢? 答案是:替它誕生一個Looper對象和一個MQ就行了。茲撰寫個范例程序Rx03如下:// ac01.java //…… public class ac01 extends Activity implements OnClickListener {private Thread t;private Handler h;private String str;public void onCreate(Bundle icicle) {//……..t = new Thread(new Task());t.start(); }public void onClick(View v) {switch(v.getId()){case 101:Message m = h.obtainMessage(1, 33, 1, null);h.sendMessage(m); break;case 102: setTitle(str); break;case 103: h.getLooper().quit(); finish(); break;}}class Task implements Runnable {public void run() {Looper.prepare();h = new Handler(){public void handleMessage(Message msg) {str = Thread.currentThread().getName() +", value=" + String.valueOf(msg.arg1);}};Looper.loop();}} }Step-1: 一開始,由主線程執行onCreate()函數。主線程繼續執行到指令: t = new Thread(new Task()); t.start(); 就誕生一個子線程,并啟動子線程去執行Task的run()函數,而主線程則返回到信息回圈,并持續監視MQ的動態了。Step-2: 此時,子線程執行到run()函數里的指令:Looper.prepare(); 就誕生一個Looper對象,準備好一個信息回圈(Message Loop) 和MQ數據結構。繼續執行到指令: h = new Handler(){ //….. } 就誕生一個Handler對象,可協助將信息丟到子線程的MQ上。接著繼續執行到指令: Looper.loop(); 也就開始執行信息回圈,并持續監視子線程的MQ動態了。Step-3: 當用戶按下UI按鈕時,UI事件發生了,Android將此UI事件的信息丟到主線程的MQ,主線程就執行onClick()函數里的指令: Message m = h.obtainMessage(1, 33, 1, null); h.sendMessage(m); 主線程藉由h將該Message對象(內含整數值33)丟入子線程的MQ里面,然后主線程返回到它的信息循環(Looper),等待UI畫面的事件或信息。Step-4: 子線程看到MQ有了信息,就會取出來,調用handleMessage()函數: public void handleMessage(Message msg) {str = Thread.currentThread().getName() +", value=" + String.valueOf(msg.arg1);} 來處理之,就設定的str的值。請留意,此刻子線程因為不能碰觸UI控件,所以無法直接將str值顯示于畫面上。Step-5: 當用戶按下<show value>按鈕時,主線程就執行onClick()函數,將str值顯示于畫面上。于是,實現主線程與子線程之間的雙向溝通了。45 - 認識線程(Thread)模式f
6. Android UI的單線程環境
單線程程序概念
單線程程序意謂著兩個(或多個)線程不能共享對象或變量值。Android的UI是單線程程序的環境。 UI控件(如Button等)都是由UI線程所創建,內部攸關于UI顯示的屬性或變量都只有UI線程才能存取(Access)之,別的線程并不能去存取之。例如下圖里的View類別體系,都只限于UI線程才能去執行它們的onDraw()函數,因為它會實際更動到UI的屬性。 public class myActivity extends Activity implements OnClickListener {private Button ibtn;@Override protected void onCreate(Bundle icicle) {super.onCreate(icicle);ibtn = new Button(this);//…………….}// 其它函數 }由于UI線程來執行onCreate()函數,誕生了Button對象,因而只限UI線程能去存取該對象里攸關UI的屬性,其它線程不能去碰它們。線程安全問題就是如何避免不同線程之間,可能會相互干擾的問題。 雖然兩個線程幾乎同時先后執行一個類別里的(可能不同)函數,只要不共享對象、或共享變量(例如Android的UI單線程環境),就不會產生干擾現象,也就沒有線程安全問題。換句話說,如果各自使用自己的對象或變量(即不共享對象或變量),就不會干擾到別線程執行的正確性了。 例如下述范例:// Ex01.java class Task2{private int count;public void init(){ count = 0; }public void f1() {for(int i=0; i<3; i++) {count++;try {Thread.sleep(10);} catch (InterruptedException e) { e.printStackTrace();}System.out.println(Thread.currentThread().getName() +"'s count: " + count);}} } class Task implements Runnable {public void run() {Task2 ta2 = new Task2();ta2.init(); ta2.f1();} }public class JMain {public static void main(String[] args) {Task ta = new Task();Thread t1 = new Thread( ta, "A");Thread t2 = new Thread( ta, "B");t1.start();t2.start();System.out.println("Waiting...");} }這里,t1和t2線程共享主線程所誕生的ta對象,但是各自誕生了Task2類別之對象。兩者各自使用自己的對象(即不共享對象或變量),就不會干擾到別線程的數據。所以輸出正確的結果:SurfaceView與非UI線程
View控件是由UI 線程(主線程)所執行。如果需要去迅速更新UI畫面或者UI畫圖需要較長時間(避免阻塞主線程),就使用SurfaceView。 它可以由背景線程(background thead)來執行,而View只能由UI(主)線程執行畫面顯示或更新。 在SurfaceView里,非UI線程可以去碰觸UI顯示,例如將圖形繪制于Surface畫布上。這SurfaceView內含高效率的rendering機制,能讓背景線程快速更新surface的內容,適合演示動畫(animation)。46 - 認識線程(Thread)模式g
7. 線程安全的化解之例
View是一個單線程的類;其意味著:此類的撰寫著心中意圖只讓有一個線程來執行這個類的代碼(如函數調用)。 // ac01.java // …….. public class ac01 extends Activity implements OnClickListener {private Button btn;public void onCreate(Bundle icicle) {// ……..btn = new Button(this);btn.setText(“Exit");// ……..}public void f1() {// ……..btn.setText(“OK");// ……..} }同樣地,View的子類開發者也不宜讓多線程去執行View(基類)的代碼。// …… public class ac01 extends Activity {@Override public void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);okButton ok_btn = new okButton(this);LinearLayout.LayoutParams param =new LinearLayout.LayoutParams(ok_btn.get_width(),ok_btn.get_height());// ……..}}/* ---- okButton ---- */ // ………. public class okButton extends Button{public okButton(Context ctx){super(ctx);super.setText("OK");super.setBackgroundResource(R.drawable.ok_blue);}public void f1() {super.setText("Quit");}public int get_width(){ return 90; }public int get_height(){ return 50; } }如果共享對象或變量是不可避免的話,就得試圖錯開線程的執行時刻了。 由于共享對象或變量,若兩個線程會爭相更改對象的屬性值或變量值時,則可能會互相干擾對方的計算過程和結果。 例如:class Task implements Runnable {private int count;public void init(){ count = 0; }public void f1() {for(int i=0; i<3; i++) {count++;try {Thread.sleep(10);} catch (InterruptedException e) { e.printStackTrace(); }System.out.println(Thread.currentThread().getName() +"'s count: " + count);}}public void run() {this.init(); this.f1();} }public class JMain {public static void main(String[] args) {Task ta = new Task();Thread t1 = new Thread( ta, "A");Thread t2 = new Thread( ta, "B");t1.start();t2.start();System.out.println("Waiting...");} } 由于在這個程序只會誕生myActivity對象,卻可能誕生多個Thread對象,可能出現多條線程同時并行(Concurrently)執行run()函數的情形。此時必須特別留意線程沖突問題。也就是多條線程共享變量或對象,導致互相干擾計算中的變量值,因而產生錯誤的計算結果。例如,依據上圖的設計結構,撰寫程序碼,可能無意中這會產生沖突了。 ? 如下范例// myActivity.java //………. public class myActivity extends Activity implements OnClickListener, Runnable {private Button ibtn;private int sum;@Overrideprotected void onCreate(Bundle icicle) {//………Thread th1 = new Thread(this); th1.start();Thread th2 = new Thread(this); th2.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}setTitle(String.valueOf(sum)); }public void onClick(View v) {finish(); } //------------------------------------------ @Override public void run() {sum = 0;for(int i=0; i<10000; i++ )sum += 1; } }第一個線程還沒做完run()函數的計算,其后的第二個線程就進來run()函數,并行共享了sum變量值,因而輸出錯誤的結果:11373。此時,可以使用synchronized機制來錯開兩個線程,就正確了。例如將上數程序碼 修改如下:// ………… int sum; Thread th1 = new Thread(this); th1.start(); Thread th2 = new Thread(this); th2.start(); Thread.sleep(1000); setTitle(String.valueOf(sum)); // …………. @Override public void run() {this.exec(); } public synchronized void exec(){sum = 0;for(int i=0; i<10000; i++ ) sum += 1; } // end第二個線程會等待第一個線程離開exec()函數之后才能進入exec(),就不會產生共享sum變量值的現象了。由于變量就存于對象內部,如果不共享對象,就可避免共享內部變量的問題。47 - 應用Android的UI框架a
以設計游戲循環(GameLoop)為例
1. UI線程、View與onDraw()函數
1.游戲的UI畫面通常是由大量美工貼圖所構成的,并不會使用一般的Layout來布局,而是使用畫布(Canvas)來把圖片顯示于View的窗口里。 2.在View類里有個onDraw()函數,View類體系里的每一個類都必須覆寫(Override) 這 個onDraw()函數,來執行實際繪圖的動作。 游戲的基本動作就是不斷的進行:繪圖和刷新(Refresh)畫面。其中,onDraw()函數實踐畫圖,將圖形繪制于View的畫布(Canvas)上,并顯示出來;而invalidate()函數則啟動畫面的刷新,重新調用一次onDraw()函數。當我們設計myView子類別時,也必須覆寫onDraw()函數。在程序執行時,Android框架會進行反向調用到myView的onDraw()函數來進行畫圖動作。如下圖:2. 基本游戲循環(GameLoop)
游戲的基本動作就是不斷的繞回圈(Loop),重復繪圖和刷新畫面的動作。最簡單的循環實現方式是:在onDraw()函數里調用invalidate()函數,就能刷新畫面(重新調用一次onDraw()函數)了。 // myView.java // ……… public class myView extends View {private Paint paint= new Paint();private int line_x = 100, line_y = 100;private float count = 0;myView(Context ctx) { super(ctx); }@Override protected void onDraw(Canvas canvas) {super.onDraw(canvas);//-----------------------------------------------------if( count > 12) count = 0;int x = (int) (75.0 * Math.cos(2*Math.PI * count/12.0));int y = (int) (75.0 * Math.sin(2*Math.PI * count/12.0));count++;//---------------------------------------------canvas.drawColor(Color.WHITE);paint.setColor(Color.BLACK);paint.setStrokeWidth(3);canvas.drawLine(line_x, line_y, line_x+x, line_y+y, paint);paint.setStrokeWidth(2);paint.setColor(Color.RED);canvas.drawRect(line_x-5, line_y - 5, line_x+5, line_y + 5, paint);paint.setColor(Color.YELLOW);canvas.drawRect(line_x-3, line_y - 3, line_x+3, line_y + 3, paint);try {Thread.sleep(1000);} catch (InterruptedException ie) {}invalidate();} }Android中提供了invalidate()來實現畫面的刷新:即觸發框架重新執行onDraw()函數來繪圖及顯示。3. 使用UI線程的MQ(Message Queue)
// myView.java // ……… public class myView extends View { // ……… @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // ……… // canvas.drawRect(….); invalidate(); } }我們可以透過Message方式來觸發UI線程去調用invalidate()函數,而達到重新執行onDraw()來進行重復繪圖和刷新畫面的動作。// myView.java //…….. public class myView extends View {private Paint paint= new Paint();private int line_x = 100, int line_y = 100;private float count = 0;private myHandler h;myView(Context ctx){ super(ctx); h = new myHandler(); }@Override protected void onDraw(Canvas canvas) {super.onDraw(canvas);if( count > 12) count = 0;int x = (int) (75.0 * Math.cos(2*Math.PI * count/12.0));int y = (int) (75.0 * Math.sin(2*Math.PI * count/12.0));count++;canvas.drawColor(Color.WHITE);paint.setColor(Color.RED);paint.setStrokeWidth(3);canvas.drawLine(line_x, line_y, line_x+x, line_y+y, paint);paint.setStrokeWidth(2);paint.setColor(Color.BLUE);canvas.drawRect(line_x-5, line_y - 5, line_x+5, line_y + 5, paint);paint.setColor(Color.YELLOW);canvas.drawRect(line_x-3, line_y - 3, line_x+3, line_y + 3, paint);h.removeMessages(0);Message msg = h.obtainMessage(0);h.sendMessageDelayed(msg, 1000);}class myHandler extends Handler {@Override public void handleMessage(Message msg) {invalidate();}}; }使用sendMessageDelayed()函數來暫停一下,延遲數秒鐘才傳遞 Message給UI線程4. 誕生一個小線程,擔任游戲線程
剛才是由UI線程來丟Message到自己的MQ里;也就是UI線程丟Message給自己。同一樣地,也可以由其它線程來丟Message到UI線程的MQ里,來觸發UI線程去調用invalidate()函數。// myView.java // ………. public class myView extends View {private Paint paint= new Paint();private int line_x = 100, line_y = 100;private float count = 0;private myHandler h;myView(Context ctx) { super(ctx); h = new myHandler(); }@Override protected void onDraw(Canvas canvas) {super.onDraw(canvas);if( count > 12) count = 0;int x = (int) (75.0 * Math.cos(2*Math.PI * count/12.0));int y = (int) (75.0 * Math.sin(2*Math.PI * count/12.0));count++;canvas.drawColor(Color.WHITE);paint.setColor(Color.RED);paint.setStrokeWidth(3);canvas.drawLine(line_x, line_y, line_x+x, line_y+y, paint);paint.setStrokeWidth(2);paint.setColor(Color.BLUE);canvas.drawRect(line_x-5, line_y - 5, line_x+5, line_y + 5, paint);paint.setColor(Color.MAGENTA);canvas.drawRect(line_x-3, line_y - 3, line_x+3, line_y + 3, paint);//--------------------------------myThread t = new myThread();t.start();}// 誕生一個小線程,擔任游戲線程,負責回圈控制 class myThread extends Thread{public void run() {h.removeMessages(0);Message msg = h.obtainMessage(0);h.sendMessageDelayed(msg, 1000);} }; class myHandler extends Handler {@Override public void handleMessage(Message msg) {invalidate(); // call onDraw()}}; }UI線程誕生一個小線程,并且由該小線程去執行myThread類別里的run()函數。接著,這新線程執行到指令:h.removeMessages(0); Message msg = h.obtainMessage(0); h.sendMessageDelayed(msg, 1000);延遲數秒鐘才傳遞 Message給UI線程(丟 入UI線程的MQ里)。 ? 當UI線程發現MQ有個Message,就去執行myHandler類別里的handleMessage()函數。就觸發UI線程去調用invalidate()函數了。48 - 應用Android的UI框架b
5. 小線程調用postInvalidate()
剛才的小線程傳遞Message給UI線程(丟入UI線程的MQ里),觸發UI線程去調用invalidate()函數。Android提供一個postInvalidate()函數來替代上述的動作。由小線程直接去調用postInvalidate()函數,就能間接觸發UI線程去調用invalidate()函數了。// myView.java //…… public class myView extends View {//……….@Override protected void onDraw(Canvas canvas) {//…………..myThread t = new myThread();t.start();}class myThread extends Thread{public void run() {postInvalidateDelayed(1000);}}; }由小線程直接去調用postInvalidate()函數;就相當于,由小線程傳遞Message給UI線程,觸發UI線程去調用invalidate()函數。49 - 應用Android的UI框架c
6. 設計一個GameLoop類別
剛才的小線程,其實就扮演了游戲線程(Game thread)的角色,它負責控制游戲的循環。 // myView.java //…… public class myView extends View {//……….@Override protected void onDraw(Canvas canvas) {//…………..myThread t = new myThread();t.start();}class myThread extends Thread{public void run() {postInvalidateDelayed(1000);}}; }于是,我們將剛才的小線程部分獨立出來,成為一個獨立的類別,通稱為游戲線程(Game Thread) 或游戲循環(Game Loop)。 // GameLoop.java // ……… public class GameLoop extends Thread {myView mView;GameLoop(myView v){mView = v;}public void run() {mView.onUpdate();mView.postInvalidateDelayed(1000);} }// myView.java // ……….. public class myView extends View {private Paint paint= new Paint();private int x, y;private int line_x = 100;private int line_y = 100;private float count = 0;myView(Context ctx) {super(ctx);}public void onUpdate(){if( count > 12) count = 0;x = (int) (75.0 * Math.cos(2*Math.PI * count/12.0));y = (int) (75.0 * Math.sin(2*Math.PI * count/12.0));count++;}@Override protected void onDraw(Canvas canvas) {super.onDraw(canvas);canvas.drawColor(Color.WHITE);paint.setColor(Color.BLUE);paint.setStrokeWidth(3);canvas.drawLine(line_x, line_y, line_x+x, line_y+y, paint);paint.setStrokeWidth(2);paint.setColor(Color.RED);canvas.drawRect(line_x-5, line_y - 5, line_x+5, line_y + 5, paint);paint.setColor(Color.CYAN);canvas.drawRect(line_x-3, line_y - 3, line_x+3, line_y + 3, paint);//--------------------------------GameLoop loop = new GameLoop(this);;loop.start();} }首先由myActivity來誕生myView對象,然后由Android框架調用myView的onDraw()函數來繪圖和顯示。繪圖完畢,立即誕生一個GameLoop對象,并調用start()函數去啟動一個小線程去調用postInvalidate()函數。就觸發UI線程重新調用myView的onDraw()函數。50 - 應用Android的UI框架d
7. 只誕生一次GameLoop對象
每次執行onDraw()時,都會重新誕生一次GameThread對象,也誕生一次游戲線程去調用postInvalidate()函數。似乎是UI線程控制著游戲線程,這樣游戲線程就不能扮演主控者的角色了。 于是,可換一個方式:一開始先誕生一個游戲線程,并且使用while(true)來創造一個無限循環(Endless Loop),讓游戲線程持續繞回圈,而不會停止。 在誕生myView時,就誕生GameLoop對象,且調用其start()函數來啟動游戲線程。此時游戲線程處于<暫停>狀態,雖然繼續繞回圈,但是并不會調用postInvalidate()函數。接著,由Android框架調用myView的onDraw()函數來繪圖和顯示。繪圖完畢,立即調用GameLoop的loopResume()函數,讓GameLoop從<暫 停>狀態轉移到<執行>狀態。此時,這游戲線程就去調用postInvalidate()函數,觸發UI線程重新調用myView的onDraw()函數。如下圖: // GameLoop.java // …….. public class GameLoop extends Thread { private myView mView; private boolean bRunning; GameLoop(myView v){mView = v; bRunning = false; } public void run() {while(true){if(bRunning){mView.onUpdate();mView.postInvalidateDelayed(1000);loopPause();} } } public void loopPause(){ bRunning = false; } public void loopResme(){ bRunning = true; } }其中,loopPause()函數將bRunning設定為false,游戲線程就處于<暫停>狀態。loopResume()函數將bRunning設定為true,游戲線程就處于<執行狀態,就會調用myView的onUpdate()函數,去更新繪圖的設定。然后調用postInvalidate()函數,觸發UI線程去重新調用onDraw()函數。// myView.java // ……… public class myView extends View {private Paint paint= new Paint();private int x, y;private int line_x = 100, line_y = 100;private float count;private GameLoop loop;myView(Context ctx) {super(ctx);init();loop = new GameLoop(this);loop.start();}public void init(){count = 0;x = (int) (75.0 * Math.cos(2*Math.PI * count/12.0));y = (int) (75.0 * Math.sin(2*Math.PI * count/12.0));}public void onUpdate(){ // 游戲線程執行的if( count > 12) count = 0;x = (int) (75.0 * Math.cos(2*Math.PI * count/12.0));y = (int) (75.0 * Math.sin(2*Math.PI * count/12.0));count++;}@Override protected void onDraw(Canvas canvas) {super.onDraw(canvas);canvas.drawColor(Color.WHITE);paint.setColor(Color.BLUE);paint.setStrokeWidth(3);canvas.drawLine(line_x, line_y, line_x+x, line_y+y, paint);paint.setStrokeWidth(2);paint.setColor(Color.RED);canvas.drawRect(line_x-5, line_y - 5, line_x+5,line_y + 5, paint);paint.setColor(Color.CYAN);canvas.drawRect(line_x-3, line_y - 3, line_x+3, line_y + 3, paint);//--------------------------------loop.loopResme();} }請留意:onUpdate()函數是由游戲線程所執行的;而onDraw()則是由UI線程所執行的。51 - SurfaceView的UI多線程a
1. View與SurfaceView之區別
SurfaceView是View的子類,其內嵌了一個用來繪制的Surface。 ? 當SurfaceView成為可見時,就會誕生Surface;反之當SurfaceView被隱藏時,就會刪除Surface,以便節省資源。 程序里可以控制Surface的大小,SurfaceView可控制Surface的繪圖位置。 View組件是由UI 線程(主線程所執行)。如果需要去迅速更新UI畫面或者UI畫圖需要較長時間(避免阻塞主線程),就使用SurfaceView。 它可以由背景線程(background thead)來執行,而View只能由UI(主)線程執行。這SurfaceView內含高效率的rendering機制,能讓背景線程快速更新surface的內容,適合演示動畫(animation)。 在程序里,可以通過SurfaceHolder接口來處理Surface,只要調用getHolder()函數就可以取得此接口。 當SurfaceView成為可見時,就會誕生Surface;反之當SurfaceView被隱藏時,就會刪除Surface,以便節省資源。當Surface誕生和刪除時,框架互調用SurfaceCreated()和 SurfaceDestroyed()函數。
52 - SurfaceView的UI多線程b
2. 使用SurfaceView畫2D圖
以SurfaceView繪出Bitmap圖像
設計SpriteView類別來實作SurfaceHolder.Callback接口
首先來看個簡單的程序,顯示出一個Bitmap圖像。這個圖像就構成Sprite動畫的基本圖形。這個圖像如下:
設計GameLoop類別把小線程移出來
// SpriteView.java // …….. public class SpriteView implements SurfaceHolder.Callback{private SpriteThread sThread;private Paint paint;private Bitmap bm;public SpriteView(Bitmap bmp) { bm = bmp; }@Overridepublic void surfaceCreated(SurfaceHolder holder) {sThread = new SpriteThread(holder, this);sThread.start();}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) { }@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width,int height) { }protected void onDraw(Canvas canvas) {paint= new Paint();canvas.drawColor(Color.WHITE);canvas.drawBitmap(bm, 10, 10, paint);} }// SpriteThread.java//………. public class SpriteThread extends Thread {private SpriteView mView;private SurfaceHolder mHolder;private Canvas c;SpriteThread(SurfaceHolder h, SpriteView v){mHolder = h; mView = v;}public void run(){try{c = mHolder.lockCanvas(null);synchronized (mHolder){ mView.onDraw(c); } }finally{if(c!=null){ mHolder.unlockCanvasAndPost(c); }}} } // myActivity.java // …….. public class myActivity extends Activity implements OnClickListener {private SurfaceView sv = null;private Button ibtn;private Bitmap bm;private SpriteView spView;@Override protected void onCreate(Bundle icicle) {super.onCreate(icicle);LinearLayout layout = new LinearLayout(this);layout.setOrientation(LinearLayout.VERTICAL);sv = new SurfaceView(this);bm = BitmapFactory.decodeResource(getResources(), R.drawable.walk_elaine);spView = new SpriteView(bm);sv.getHolder().addCallback(spView);LinearLayout.LayoutParams param =new LinearLayout.LayoutParams(200, 200);param.topMargin = 10; param.leftMargin = 10;layout.addView(sv, param);//----------------------------------------------ibtn = new Button(this); ibtn.setOnClickListener(this);ibtn.setText("Exit");ibtn.setBackgroundResource(R.drawable.gray);LinearLayout.LayoutParams param1 =new LinearLayout.LayoutParams(200, 65);param1.topMargin = 10; param1.leftMargin = 10;layout.addView(ibtn, param1);setContentView(layout);}public void onClick(View v) { finish(); } }讓圖像在SurfaceView里旋轉
53 - AIDL與Proxy-Stub設計模式a
1. 復習:IBinder接口
54 - AIDL與Proxy-Stub設計模式b
2. IBinder接口的一般用途
55 - AIDL與Proxy-Stub設計模式c
3. 包裝IBinder接口-- 使用Proxy-Stub設計模式
采用Proxy-Stub設計模式將IBinder接口包裝起來,讓App與IBinder接口不再產生高度相依性。 其將IBinder接口包裝起來,轉換出更好用的新接口: Proxy類提供較好用的IA接口給Client使用。 Stub類別則是屏蔽了Binder基類的onTransact()函數,然后將IA接口里的f1()和f2()函數定義為抽象函數。于是簡化了 App開發的負擔:
4. 誰來寫Proxy及Stub類呢?-- 地頭蛇(App開發者)自己寫
在這個范例里,定義了一個IPlayer接口,然后規劃了PlayerProxy和PlayerStub兩的類,如下圖: 定義一個新接口:IPlayer// IPlayer.java package com.misoo.pkgx; public interface IPlayer {void play();void stop();String getStatus(); }撰寫一個Stub類:PlayerStub// PlayerStub.java package com.misoo.pkgx; import android.os.Binder; import android.os.Parcel; public abstract class PlayerStub extends Binder implements IPlayer{@Override public boolean onTransact(int code, Parcel data,Parcel reply, int flags) throws android.os.RemoteException {reply.writeString(data.readString()+ " mp3");if(code == 1) this.play();else if(code == 2) this.stop();return true;}public abstract void play();public abstract void stop();public abstract String getStatus(); }撰寫一個Proxy類:PlayerProxy// PlayProxy.java private class PlayerProxy implements IPlayer{private IBinder ib;private String mStatus;PlayerProxy(IBinder ibinder) { ib = ibinder; }public void play(){Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();data.writeString("playing");try {ib.transact(1, data, reply, 0);mStatus = reply.readString();} catch (Exception e) {e.printStackTrace();}}public void stop(){Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();data.writeString("stop");try { ib.transact(2, data, reply, 0);mStatus = reply.readString();} catch (Exception e) {e.printStackTrace();}}public String getStatus() { return mStatus; }} }撰寫mp3Binder類// mp3Binder.java // …….. public class mp3Binder extends PlayerStub{private MediaPlayer mPlayer = null;private Context ctx;public mp3Binder(Context cx){ ctx= cx; }public void play(){if(mPlayer != null) return;mPlayer = MediaPlayer.create(ctx, R.raw.test_cbr);try {mPlayer.start();} catch (Exception e) {Log.e("StartPlay", "error: " + e.getMessage(), e);}}public void stop(){if (mPlayer != null) { mPlayer.stop(); mPlayer.release();mPlayer = null; }}public String getStatus() { return null; } }撰寫mp3RemoteService類// mp3RemoteService.java package com.misoo.pkgx; import android.app.Service; import android.content.Intent; import android.os.IBinder; public class mp3RemoteService extends Service {private IBinder mBinder = null;@Overridepublic void onCreate() {mBinder = new mp3Binder(getApplicationContext());}@Overridepublic IBinder onBind(Intent intent) {return mBinder; } }// ac01.java // ……… public class ac01 extends Activity implements OnClickListener {//……….private PlayerProxy pProxy = null;public void onCreate(Bundle icicle) {// ………startService(new Intent("com.misoo.pkgx.REMOTE_SERVICE"));bindService(new Intent("com.misoo.pkgx.REMOTE_SERVICE"),mConnection, Context.BIND_AUTO_CREATE); }private ServiceConnection mConnection =new ServiceConnection() {public void onServiceConnected(ComponentName className,IBinder ibinder){ pProxy = new PlayerProxy(ibinder); }public void onServiceDisconnected(ComponentName classNa){}};public void onClick(View v) {switch (v.getId()) {case 101: pProxy.play(); tv.setText(pProxy.getStatus());break;case 102: pProxy.stop(); tv.setText(pProxy.getStatus());break;case 103:unbindService(mConnection);stopService(new Intent("com.misoo.pkgx.REMOTE_SERVICE"));finish(); break;}}} PlayerStub類將onTransact()函數隱藏起來,提供一個更具有美感、更親切的新接口給mp3Binder類使用。 隱藏了onTransact()函數之后,mp3Binder類的開發者就不必費心去了解onTransact()函數了。于是,PlayerProxy與PlayerStub兩個類遙遙相對,并且將IPC細節知識(例如transact()和onTransact()函數之參數等)包夾起來。56 - AIDL與Proxy-Stub設計模式d
5. 誰來寫Proxy及Stub類呢? --強龍提供AIDL工具,給地頭蛇產出Proxy和Stub類
由框架開發者來撰寫Proxy-Stub類,才能減輕開發者的負擔。 框架分為:<天子框架>和<曹操框架>。 因此,應該由兩者(天子或曹操)之一來撰寫Proxy-Stub類。但是,有個難題:IA接口(如下圖所示)的內容必須等到<買主>來了才會知道。 在框架開發階段,買主還沒來,IA接口的知識無法取得,又如何定義IA接口呢? 沒有IA接口定義,又如何撰寫Stub和Proxy類呢? 好辦法是:“強龍(天子或曹操)撰寫代碼(在先) ;然后,地頭蛇(App開發者)定義接口(在后)。”技術之一是:類別模板(class template) 例如,強龍撰寫模板: template< class T > class SomeClass {private:T data;public:SomeClass() { }void set(T da){ data = da; } };地頭蛇利用模板來生成一個類:SomeClass<Integer> x;由于接口(interface)是一種特殊的類(class),所以也可以定義模板如下:template<interface I> class BinderProxy{// ………};地頭蛇利用模板來生成一個類:BinderProxy<IPlayer> proxy;除了模板之外,還有其它編程技術可以實現<強龍寫代碼,地頭蛇定義接口>的方案嗎?AIDL的目的是定義Proxy/Stub來封裝IBinder接口,以便產生更親切貼心的新接口。 所以,在應用程序里,可以選擇使用IBinder接口,也可以使用AIDL來定義出新接口。由于IBinder接口只提供單一函數(即transact()函數)來進行遠距通信,呼叫起來比較不方便。 所以Android提供aidl.exe工具來協助產出Proxy和Stub類別,以化解這個困難。只要你善于使用開發環境的工具(如Android的aidl.exe軟件工具)自動產生Proxy和Stub類別的程序代碼;那就很方便了。此范例使用Android-SDK的/tools/里的aidl.exe工具程序,根據接口定義檔(如下述的mp3PlayerInterface.aidl)而自動產出Proxy及Stub類別,其結構如下: 藉由開發工具自動產出Proxy及Stub類的代碼,再分別轉交給ac01和mp3Binder開發者。此范例程序執行時,出現畫面如下:依據UI畫面的兩項功能:<Play>和< Stop>,以Java定義接口,如下的代碼:// mp3PlayerInterface.aidl interface mp3PlayerInterface mp3PlayerInterface{void play();void stop(); }使用Android-SDK所含的aidl.exe工具,將上述的mp3PlayerInterface.aidl檔翻譯成為下述的mp3PlayerInterface.java檔案。// mp3PlayerInterface.java /* * This file is auto-generated. DO NOT MODIFY. * Original file: mp3PlayerInterface.aidl */ // ……… public interface mp3PlayerInterface extends android.os.IInterface { /** Local-side IPC implementation stub class. */ public static abstract class Stub extends android.os.Binder implements com.misoo.pkgx.mp3PlayerInterface { // ………. public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { switch (code){ case INTERFACE_TRANSACTION:{ reply.writeString(DESCRIPTOR); return true; } case TRANSACTION_play:{ data.enforceInterface(DESCRIPTOR); this.play(); reply.writeNoException(); return true; } case TRANSACTION_stop:{ data.enforceInterface(DESCRIPTOR); this.stop(); reply.writeNoException(); return true; }} return super.onTransact(code, data, reply, flags); }private static class Proxy implements com.misoo.pkgx.mp3PlayerInterface { private android.os.IBinder mRemote; //…………. public void play() throws android.os.RemoteException { android.os.Parcel _data = android.os.Parcel.obtain(); android.os.Parcel _reply = android.os.Parcel.obtain(); try { _data.writeInterfaceToken(DESCRIPTOR); mRemote.transact(Stub.TRANSACTION_play, _data, _reply, 0); _reply.readException(); } finally { _reply.recycle(); _data.recycle(); } }public void stop() throws android.os.RemoteException { android.os.Parcel _data = android.os.Parcel.obtain(); android.os.Parcel _reply = android.os.Parcel.obtain(); try { _data.writeInterfaceToken(DESCRIPTOR); mRemote.transact(Stub.TRANSACTION_stop, _data, _reply, 0); _reply.readException(); } finally { _reply.recycle(); _data.recycle(); }}} static final int TRANSACTION_play = (IBinder.FIRST_CALL_TRANSACTION + 0); static final int TRANSACTION_stop = (IBinder.FIRST_CALL_TRANSACTION + 1); } public void play() throws android.os.RemoteException; public void stop() throws android.os.RemoteException; }表面上,此mp3PlayerInterface.java是蠻復雜的,其實它的結構是清晰又簡單的,只要對于類繼承、反向調用和接口等面向對象觀念有足夠的認識,就很容易理解了。// mp3Binder.java package com.misoo.pkgx; import android.content.Context; import android.media.MediaPlayer; import android.util.Log; public class mp3Binder extends mp3PlayerInterface.Stub{ private MediaPlayer mPlayer = null; private Context ctx; public mp3Binder(Context cx){ ctx= cx; } public void play(){if(mPlayer != null) return;mPlayer = MediaPlayer.create(ctx, R.raw.test_cbr);try { mPlayer.start();} catch (Exception e){ Log.e("StartPlay", "error: " + e.getMessage(), e); }} public void stop(){if (mPlayer != null){ mPlayer.stop(); mPlayer.release(); mPlayer = null; } } }撰寫mp3RemoteService類// mp3Service.java package com.misoo.pkgx; import android.app.Service; import android.content.Intent; import android.os.IBinder; public class mp3Service extends Service {IBinder ib = null;@Override public void onCreate() {super.onCreate();ib = new mp3Binder(this.getApplicationContext()); }@Override public void onDestroy() { }@Override public IBinder onBind(Intent intent) {return ib;} }// ac01.java // ……… public class ac01 extends Activity implements OnClickListener {//……….private PlayerProxy pProxy = null;public void onCreate(Bundle icicle) {// ………startService(new Intent("com.misoo.pkgx.REMOTE_SERVICE"));bindService(new Intent("com.misoo.pkgx.REMOTE_SERVICE"), mConnection, Context.BIND_AUTO_CREATE); }private ServiceConnection mConnection = new ServiceConnection() {public void onServiceConnected(ComponentName className,IBinder ibinder) {pProxy = mp3PlayerInterface.Stub.asInterface(ibinder);}public void onServiceDisconnected(ComponentName className) {}};public void onClick(View v) {switch (v.getId()) {case 101: pProxy.play(); tv.setText(pProxy.getStatus()); break;case 102: pProxy.stop(); tv.setText(pProxy.getStatus()); break;case 103:unbindService(mConnection);stopService(new Intent("com.misoo.pkgx.REMOTE_SERVICE"));finish(); break;}} }對于Anrdoid的初學者而言, Android的AIDL機制可說是最難弄懂的。57 - 活用IBinder接口于近程通信a
1. 在同一進程里,活用IBinder接口
1. myActivity對象是誰創建的呢? 2. myService對象是誰創建的呢? 3. 當myService類里有個f1()函數,如何去調用它呢? 4. 必須先取得myService對象的指針,才能調用f1()函數去存取對象的屬性(Attribute)值。 5. 那么,該如何才能取得myService對象的指針呢? 6. 想一想,可以透過myService類的靜態(static)屬性或函數來取得myService對象的指針嗎? 7. 可以透過IBinder接口來取得myService對象的指針嗎?IBinder接口的重要目的是支持跨進程的遠程調用。然而,它也應用于同一進程里的近程調用。 例如,當Activity遠程調用Service時,我們常用bindService()函數去綁定Service,取得對方的IBinder接口。 在近程(同一進程內)調用時也可以使用bindService()函數去綁定Service,并取得對方的IBinder接口。IBinder接口的典型實現類是Binder基類,其定義于Binder.java檔案里。 近程通信(同一進程里)如何使用IBinder接口呢? 舉例說明之。 例如,myActivity和myService兩者都執行于同一個進程(process)里,而且myActivity提供一個IS接口,其定義如下:interface IS {void f1();void f2(); } 現在,myActivity想要透過此IS接口來調用myService的函數;如下圖:2. 目的、議題與方法
目的:myActivity想去直接(近程)調用myService類的函數, 例如IS接口里的f1()函數 議題:如何取的myService對象的IS接口呢? 方法:先取得myService對象的IBinder接口步驟是:
Step-1. myActivity透過bindService()函數來綁定(Bind)此myService。 Step-2. myService回傳myBinder類的IBinder接口給myActivity。 Step-3. myActivity將IBinder接口轉換為myBinder類的接口 Step-4. myActivity調用myBinder類的getService()函數,取得myService的IS接口。 Step-5. 于是,myActivity就能調用IS接口(由myService類實現)的函數了。在Android 說明文件里,說明道:“If your service is private to your own application and runs in the same process as the client (which is common), you should create your interface by extending the Binder class and returning an instance of it from onBind(). The client receives the Binder and can use it to directly access public methods available in either the Binder implementation or even the Service.依據上述文件的說明:“… you should create your interface by extending the Binder class and returning an instance of it from onBind().” 依據這個設計圖,就來撰寫myService類別如下:// myService.java // ……….. public class myService extends Service implements IS {private final IBinder mBinder = new myBinder();//…………@Overridepublic IBinder onBind(Intent intent) {return mBinder;}//…………public class myBinder extends Binder {IS getService() {return myService.this; }public void f1(){ //……. }Public void f2() { //…… } }// myActivity.java //………. public class myActivity extends Activity {IS isv;@Overrideprotected void onCreate(Bundle savedInstanceState) {//………..Intent intent = new Intent(this, myService.class);bindService(intent, mConnection,Context.BIND_AUTO_CREATE);}private ServiceConnection mConnection = new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName className,IBinder ibinder) {myBinder ib = (myBinder)ibinder;isv = ib.getService();}}public void onClick(View v) {// …… 例如:isv.f1()} }第1步 當Android框架啟動myService時,就立即執行:private final IBinder mBinder = new myBinder();這誕生了myBinder對象。第2步 隨后,當myActivity調用bindService()時,框架會反向調用到myService的onBind()函數: public IBinder onBind(Intent intent){return mBinder; } 其將 myBinder的IBinder接口回傳給框架,并由框架調用onServiceConnected()函數,將此接口回傳給myActivity。 第3步 由于myActivity與myService在同一個進程里執行,myActivity所獲得的就是myBinder的真正接口(不是它的Proxy的); 于是,執行:myBinder ib = (myBinder) ibinder; 就從接獲的IBinder接口轉型(casting)為myBinder本身接口了。第4步 接著,執行:isv = ib.getService(); 這透過myBinder本身接口來調用getService()函數,取得了myService的IS接口。 第5步 最后,myActivity就能透過IS接口來調用myService的f1()或f2()函數了。58 - 活用IBinder接口于近程通信b
3. 留意線程的角色(用小線程執行IS或其他service的接口)
在上述的范例程序,都是由主線程所執行的。由主線程執行所有的調用。如下圖: 例如將上述onClick()函數內容改寫為: public void onClick(View v) {th1 = new Thread(this);th1.start(); }public void run() {//……. isv.f1() }就誕生小線程去調用IS接口了,如下圖: // ILoad.java // ……… interface ILoad {boolean loadImage();boolean cancel(); }// myService.java // ……… public class myService extends Service implements ILoad{private final IBinder mBinder;@Override public IBinder onBind(Intent intent) {return mBinder;}@Override public void onCreate(){super.onCreate();mBinder = new myBinder();}public class myBinder extends Binder{ILoad getService(){return myService.this;}}@Override public boolean loadImage() {// loading image from cloud}@Override public boolean cancel() {// cancel loading} }// myActivity.java // ………. public class myActivity extends Activity implements OnClickListener {ILoad isv;Thread th1;// ……..@Override public void onCreate(Bundle savedInstanceState) {// ………Intent intent = new Intent(this,myService.class);bindService(intent, mConnection,Context.BIND_AUTO_CREATE);}private ServiceConnection mConnection = new ServiceConnection(){@Override public void onServiceConnected(ComponentNameclassName, IBinder ibinder) {myBinder ib = (myBinder)ibinder;isv = ib.getService();}@Override public void onServiceDisconnected(ComponentName arg0) { }};@Override public void onClick(View v) {switch( v.getId() ){case 101:th1 = new Thread(this);th1.start();break;case 102:isv.cancel();break;default:break;}}public void run() {isv.loadImage();} }在這個范例里,活用Android框架提供的Binder基類和IBinder接口。 然后配合myService的onBind()函數,將myBinder的IBinder接口回傳給myActivity。 接著,myActivity并不透過 IBinder接口來調用myService的服務。而是直接調用了myService的IS接口。 此外,可擅用小線程來執行比較耗時的服務。59 - Messager框架與IMessager接口a
1. Messenger的概念和角色
同一進程:myActivity和myService兩者執行于同一的進程里(IPC) myActivity的線程想丟信息(Message)給myService的主線程 多條并行(Concurrently)的小線程丟信息到myService主線程的MQ, 變成同步(Synchronized)的調用myService的handleMessage()函數。 不同進程:myActivity和myService兩者執行于不同的進程里(IPC) myActivity的線程想丟信息(Message)給myService的主線程
60 - Messager框架與IMessager接口b
2. Android的Messenger框架
復習:線程、信息和IBinder接口
在Android框架里,有個IBinder接口來擔任跨進程的通訊。 在Android框架里,也有一個Message類,兩個線程之間能互傳Message對象。 于是,就能設計一個Messenger類來包裝IBinder接口機制,讓其能跨進程地將Message對象傳遞到另一個進程里,給其主線程(又稱UI線程)。 其中,由于Message類實作(Implement)了Parcelable接口,所以Messenger類可以透過IBinder接口而將Message對象傳送到另一個進程里的MessengerImpl類。 然后,Messenger透過Handler而將Message對象丟入UI線程的MQ里,讓UI線程來處理之。 在傳送Message對象之前,必須先建立MessengerImpl、Handler和myService三者之間的關系。如下圖: 首先myService誕生一個Handler對象,并誕生一個Messenger對象,并讓Messenger指向該Handler對象。 于是,Messenger對象調用Handler的getIMessenger()函數去誕生一個MessengerImpl對象,并讓Messenger對象指向MessengerImpl對象。 此時,MessengerImpl對象也指向Handler對象。 建構完畢后,在另一個進程里的myActivity就能透過Messenger類而將Message對象傳遞給MessengerImpl對象。 然后,MessengerImpl繼續將Message對象放入主線程(main thread)的MQ里,如下圖所示:步驟是:
myActivity調用bindService()去綁定myService,取得IBinder接口。 以Messenger類包裝IBinder接口。 myActivity透過Messenger類接口將Message信息傳給遠方的MessengerImpl類。 MessengerImpl類將信息丟入對方主線程的MQ里。 主線程從MQ里取得信息,并調用myService的函數來處理信息// myService.java // ………. public class myService extends Service { class myHandler extends Handler { @Override public void handleMessage(Message msg) {//……..Toast.makeText(getApplicationContext(),msg.obj.toString(),Toast.LENGTH_SHORT).show();//…….. } } }final Messenger mMessenger = new Messenger(new myHandler());@Overridepublic IBinder onBind(Intent intent) {return mMessenger.getBinder();} } // myActivity.java // ……… public class myActivity extends Activity {Messenger mMessenger = null;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.main); bindService(new Intent(this,MessengerService.class), mConnection,Context.BIND_AUTO_CREATE); }private ServiceConnection mConnection =new ServiceConnection() {public void onServiceConnected(ComponentNameclassName, IBinder ibinder){mMessenger = new Messenger(ibinder);}}public void onClick() {Message msg = Message.obtain(null, 0, “Hello”);mMessenger.send(msg);} }一開始,框架會誕生myService對象,此時也執行指令:final Messenger mMessenger = new Messenger(new myHandler()); 就誕生一個myHandler對象,并且誕生一個Messenger對象,并把myHandler對象指針存入Messenger對象里。 一旦myActivity執行到指令:bindService(new Intent(this, MessengerService.class),mConnection, Context.BIND_AUTO_CREATE); 框架會調用myService的onBind()函數,其內容為: public IBinder onBind(Intent intent) {return mMessenger.getBinder(); } 此時,調用Messenger的getBinder()函數來取的MessengerImpl的IBinder接口,并回傳給Android框架。如下圖: 接著,框架就調用myActivity的onServiceConnected()函數:public void onServiceConnected(ComponentName className, IBinder ibinder) {mMessenger = new Messenger(ibinder); } 此時,就讓Messenger對象指向IBinder接口了。 一旦myActivity執行到指令: public void onClick() {Message msg = Message.obtain(null, “hello”, 0, 0);mMessenger.send(msg); } 就誕生一個Message對象,然后調用Messenger的send()函數,此send()函數則調用IBinder接口的transact()函數,將Message對象傳遞給MessengerImpl,再透過myHandler將Message對象放入主線程的MQ里。再談線程的角色在Android文件里,寫道:“… if you want to perform IPC, but do not need to handle multithreading, implement your interface using a Messenger.” 但是,有許多人看不懂其涵意。其實,它的涵意很簡單。如果你并不考慮讓多個線程(thread)同時來執行你的Service,你就可以透過這個機制,將多個Client端(如myActivity1, myActivity2等)送來的Message對象存入單一線程的MessageQueue里,由該線程依序逐一地處理各Client傳來的Message對象。 ? 雖然多個并行的Client端線程在調用IBinder接口時,會觸發多個Binder驅動線程(Binder Thread)而進入MessengerImpl,然而它們則依序將Message丟入同一個(即主線程的)MessageQueue里。因此,對于Service而言,還是單線程的情境,你在撰寫myService程序代碼時,不必擔心多線程 之間的數據沖突問題。3. 雙向溝通的Messenger框架
? 這個Messenger框架是對Binder框架加以擴充而來的。在雙向溝通上,也繼承了Binder框架機制。 Binder框架雙向溝通的應用情境是:當myActivity透過IBinder接口調用myService的函數去執行任務時(例如使用子線程去播放mp3音樂),萬一發現底層播放系統故障了,則myService必須透過IBinder接口來通知myActivity。 ? 基于上述的IBinder雙向通信機制,就能用Messenger來加以包裝,而為跨進程雙向的Messenger框架,如下圖:
基本設計原則
61 - Messager框架與IMessager接口c
4. IMessenger接口
使用AIDL 在Messenger框架里還定義了IMessenger接口,讓應用程序(App)可直接調用IMessenger接口的send()函數。如下圖: 這是典型的Proxy-Stub模式來包裝IBinder接口。 ? 在myActivity進程里:Messenger類可以將IBinder接口轉換成為IMessenger接口。 ? 在myService進程里:也可以透過Messenger取得MessengerImpl類的IMessenger接口。62 - JNI架構原理_Java與C的對接a
1. 為什么 , Android應用需要Java和C對接呢?
63 - JNI架構原理_Java與C的對接b
2. EIT造形的Java實現
3. EIT造形的C語言實現
2.1 復習:C語言的結構(struct)
/* cx-01.c */ #include <stdio.h> struct smile{char sna;char size;float price;}; int main(void){struct smile x;x.sna = 'M';x.size = 'B';x.price = 20.5;printf( "%c, %c, %.1f", x.sna, x.size, x.price );return 0;}? 先定義結構型態──struct smile。 ? 說明了﹕struct smile型態包含char型態及float 型態的數據。進入main()函數﹐就誕生了自動變量x。 ? 此時x 變數內含sna 、size及price 三個項目。程序里以以x.sna、x.size及x.price 表示之。2.2 復習:結構指針(Pointer)
宣告結構指針﹐來指向結構變量。例如﹕ /* cx-02.c */ #include <stdio.h> #include <string.h> struct smile {char sna;float price;}; int main(void){struct smile x;struct smile *px;px = &x;px->sna = 'R';px->price = 26.8;printf( "Sna=[%c], Price=%.1f", x.sna, x.price );return 0;} ? px是struct smile型態的指針﹐x 是struct smile型態的變量﹐px可以指向x 變量。 ? “&” 運算能把x 變量的地址存入px中﹐使得px指向x 變量。 ? 指令:px->sna = 'R';px->price = 26.8; ? 把數據存入結構(變量)里。sna:'R'price:26.82.3 復習:動態內存分配
「動態」(Dynamic) 的意思是﹕待程序執行時(Run-Time)才告訴計算機共需要多少內存空間﹐計算機依照需要立即分配空間﹐裨儲存數據。 ? malloc()和free()是最常用的動態內存分配函數。如果在執行時需要空間來儲存數據宜使用malloc()函數。用完了就用free()釋放該空間。malloc()之格式為﹕指針 = malloc( 空間大小 ) ? 例如﹕ ptr = malloc(100);/* cx03.c */ #include <stdio.h> #include <malloc.h> #include <string.h> #include <stdlib.h> struct kiki {char na[10];short int age;}; typedef struct kiki NODE; int main(void) {NODE *pn;pn = (NODE *) malloc (sizeof(NODE));if( pn==NULL ){ printf("malloc() failed\n");exit(0);}strcpy( pn->na,"Alvin");pn->age = 28;printf("AGE=%d", pn->age);free(pn);return 0;}? typedef 指令定義的新型態──NODE是struct kiki 的別名。 ? 如果你計算機的sizeof(NODE)值為16﹐malloc()就索取16 bytes的空間﹐并令pn指向此區域了。64 - JNI架構原理_Java與C的對接c
2.4 以C結構表達類(class),并創建對象(object)
? 目的:要了解Java對象如何與C函數對接?
? 途徑:先了解C對象如何與C函數對接呢?
認識C函數指針
? struct里不能定義函數本身,但能定義函數指針(function pointer)屬性。typedef struct cc {int id;void (*hello)();} CC; 這個hello就是一個函數指針屬性了。
2.5 在C函數里存取對象的屬性(attribute)值
?剛才調用C函數時,其函數并沒有存取(access)對象里的屬性或數據。定義Light類typedef struct Light Light; struct Light {int state;void (*turnOn)(Light*);void (*turnOff)(Light*); };撰寫函數:static void turnOn( Light *cobj ){cobj->state = 1;printf(“ON”); } static void turnOff( Light *cobj ) {cobj->state = 0;printf(“OFF”); } struct Light *LightNew(){ // 構造式struct Light *t;t = (Light *)malloc(sizeof(Light));t->turnOn = turnOn;t->turnOff = turnOff;return (void*) t; } 創建對象,調用函數:void main() {Light *led = LightNew();led->turnOn( led );led->turnOff( led ); }
65 - JNI架構原理_Java與C的對接d
4. EIT造形的C和Java組合實現
66 - JNI架構原理_Java與C的對接e
目前對象(Current Object)指針
? 無論是C或Java都必須將目前對象(CurrentObject)指針傳給C函數。 ? 讓C函數可指向目前對象,以便存取對象的內部屬性質或調用類里的其它函數。67 - 認識JNI開發與NDKa
1. JNI基本概念
? 在Androd框架里,上層是Java框架,而下層是C/C++框架。這兩層框架之間會有密切的溝通。此時JNI(Java Native Interface)就扮演雙方溝通的接口了。 ? 藉由JNI接口,可將Java層的基類或子類的函數實作部份挖空,而移到JNI層的C函數來實作之。例如,原來在Java層有個完整的Java類: ? 這是一個完整的Java類,其add()函數里有完整的實作(Implement)代碼。如果從這Java類里移除掉add()函數里的實作代碼(就如同抽象類里的抽象函數一般),而成為本地(Native)函數;然后依循JNI接口協議而以C語言來實作之。如下圖所示:? 這個add()函數仍然是Java類的一部分,只是它是用C語言來實作而已。為什么要將Java類的add()函數挖空呢? 其主要的理由是:Java代碼執行速度較慢,而C代碼執行速度快。然而Java代碼可以跨平臺,而C代碼與本地平臺設備息息相關,所以稱之為本地(Native)代碼。 ? 在本地的C代碼里,可以創建C++類的對象,并調用其函數。如下圖:
68 - 認識JNI開發與NDKb
2. 使用Android NDK
? 當你安裝好NDK環境之后,就能動手利用NDK環境來開發本地(Native)的C程序了。 于此,茲舉例說明開發程序。 Step-1. 在Android SDK環境里,建立一個開發項目 ? 例如,建立一個名稱為NDK-01的應用程序開發項目,內含helloNDK.java和test.java程序。其中,helloNDK.java的內容如下: ? 可以將這個Java類定義,看成為這項接口的Java方敘述文件。 ? 由于這項接口,涉及兩種語言,所以應該有兩份文件,兩種語言各一份。 ? 所以,我們需要替這項接口產出一份C語言方的敘述文件,其形式就是C的頭文件(Header File)。Step-2. 進行編譯,產出helloNDK.class檔案 ? 編譯上述的項目,產生*.class檔案。 Step-3. 使用javah工具,產出C語言的*.h頭文件 ? 返回Android SDK環境,建立一個名稱為/jni/的新檔案夾(Folder)如下: ? 進入/jni/目錄區,執行javah去讀取/bin/helloNDK.class檔案,然后產出com_misoo_pk01_helloNDK.h頭文件。 ? 返回到Android SDK環境,刷新(Refresh)之后,可在Eclipse畫面上看到該頭文件如下: ? 可以打開com_misoo_pk01_helloNDK.h頭文件,其內容如下: Step-4. 依據*.h頭文件而撰寫*.c程序碼 ? 產出com_misoo_pk01_helloNDK.h頭文件之后,就可以將NDK-01開發項目內容拷貝(或只拷貝/jni/目錄區內容),拷貝到NDK的/samples/目錄里如下: ? 接著,本地C開發者就能使用C語言,結合JNI(Java Native Interface)語法,撰寫com_misoo_pk01_helloNDK.c程序碼,如下: Step-5. 編譯及連結本地程序? 必須先開啟Cygwin。也就是,從桌面或<開始/所有程序/Cygwin>里,點選<Cygwin bash shell>,進行編譯和連結動作。 ? 就完成編譯和連結任務,產出libhelloNDK.so本地程序庫,并放置于/libs/armeabi/里,如下: ? 當C開發端完成libhelloNDK.so程序庫之后,就可以將/samples/NDK-01內容(或是只拷貝/libs/目錄區內容),拷貝回去AndroidSDK環境里。于是在Android SDK環境里可以看到libhelloNDK.so本地程序庫,如下: Step-6. 編執行NDK-01范例程序 ? 此時,就可以撰寫test.java的內容,讓它調用helloNDK.java類別的本地函數,如下: Step-7. 將*.so打包到*.apk ? 接著編譯NDK-01項目,將*.so本地程序庫打包到*.apk里,并且執行該*.apk。執行到指令:obj.sayHello()時,就調用到*.so程序庫里的本地C程序。于是,test.java就將本地C程序回傳值顯示于畫面,如下:69 - 認識JNI開發與NDKc
3. 如何載入*.so檔案
VM的角色? 由于Android的應用層級類別都是以Java撰寫的,這些Java類別轉譯為Dex型式的Bytecode之后,必須仰賴Dalvik虛擬機器(VM: Virtual Machine)來執行之。VM在Android平臺里,扮演很重要的角色。VM的角色 ? 此外,在執行Java類別的過程中,如果Java類別需要與JNI本地模塊溝通時,VM就會去加載JNI本地模塊,然后讓Java的函數順利地調用到本地模塊的函數。此時,VM扮演著橋梁的角色,讓Java與本地模塊能透過標準的JNI接口而相互溝通。 ? Java層的類別是在VM上執行的,而本地模塊則不是在VM上執行,那么Java程序又如何要求VM去加載(Load)所指定的C模塊呢?可使用下述指令:System.loadLibrary(*.so的檔名);? 例如,NativeJniAdder類別,其程序碼: /* com_misoo_gx06_NativeJniAdder.c */ #include "Adder.h" #include "com_misoo_gx06_NativeJniAdder.h“ JNIEXPORT jlong JNICALLJava_com_misoo_gx06_NativeJniAdder_newObject(JNIEnv *env,jclass c){Adder* ar = (Adder*)AdderNew(); 創建一個C對象return (jlong)ar; } JNIEXPORT jlong JNICALLJava_com_misoo_gx06_NativeJniAdder_execute(JNIEnv *env, jclass c, jlong refer, jint digit_1, jint digit_2){Adder* pa = (Adder*)refer; //轉成對象的指針long result = pa->exec(digit_1, digit_2);return result; }? 就要求VM去加載Android的/system/lib/libNativeJniAdder.so檔案。載入*.so檔之后,Java類別與*.so檔就匯合起來,一起執行了。定義Adder類( Adder.h )typedef struct Adder Adder; struct Adder {int (*exec)(int a, int b); };撰寫函數struct Adder *AdderNew(){ // 構造式struct Adder *t= (Adder *)malloc(sizeof(Light));t->exec = my_exec;return (void*) t; }static int my_exec( int a, int b ){return (a + b);}將C/C++對象指針傳回Java層
? 這個JNI接口定義類別含有2個函數:newObject()和execute()。 ? 其中,newObject()函數誕生一個Adder對象,并且將該對象的指針傳遞回來給Java程序。將C/C++對象指針傳回Java層 ? 而execute()函數的refer參數,是用來讓Java程序能將對象指針傳進去給execute()函數,此時execute()就能藉由該指標而調用到先前newObject()函數所誕生的那個對象了。 ? 典型的Java程序如下述的ac01類別: newObject()將C/C++對象指針傳回java層? 在這ac01.java類別里,指令:long refer = NativeJniAdder.newObject(); ? newObject()誕生一個對象,將C/C++對象指針傳回Java層。 ? 剛才newObject()誕生一個對象,由refer儲存newObject()傳回來的對象指針。指令:int cs = (int)NativeJniAdder.execute(refer, a, b); ? 將refer傳進去給execute()函數。結語 ? VM調用<Tn>本地函數時,將 Java層對象指針(pointer)傳給<Tn>。 ? 配上<Tn>之后,<Tn>可以將C/C++對象指針回傳到Java層。 ? 由于這些Java和C代碼都在同一個進程里執行,所以指針都是可以互傳的。70 - 認識JNI開發與NDKd
? Java代碼在VM上執行。 ? 在執行Java代碼的過程中,如果Java需要與本地代碼(*.so)溝通時,VM就會把*.so視為插件<Tn>而加載到VM里。 ? 然后讓Java函數呼叫到這插件<Tn>里的C函數。 ? 插件是由VM來管理的,實體上VM是*.so插件的管理器(Plug-in Manager)。 ? Java與C函數的調用,也是透過VM來對接的。71 - 認識JNI開發與NDKe
4. *.so的入口函數:JNI_OnLoad()
? 執行System.loadLibrary()函數時,VM會反向調用*.so里的JNI_OnLoad()函數。用途有二: 1. VM詢問此*.so使用的JNI版本編號。 2. VM要求*.so做一些初期設定工作(Initialization),例如登記<函數名稱表>。? 例如,在Android的/system/lib/libmedia_jni.so檔案里,就提供了JNI_OnLoad()函數,其程序碼片段為:// #define LOG_NDEBUG 0 #define LOG_TAG "MediaPlayer-JNI" // ……… jint JNI_OnLoad(JavaVM* vm, void* reserved) {JNIEnv* env = NULL;jint result = -1;if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {LOGE("ERROR: GetEnv failed\n"); goto bail;}assert(env != NULL);if (register_android_media_MediaPlayer(env) < 0) {LOGE("ERROR: MediaPlayer native registration failed\n");goto bail;}/* success -- return valid version number */result = JNI_VERSION_1_4; bail: return result; } // KTHXBYE? 此函數回傳JNI_VERSION_1_4值給VM,于是VM知道了其所使用的JNI版本了。 ? 此外, JNI_OnLoad()函數也做了一些初期的動作,例如指令: if (register_android_media_MediaPlayer(env) < 0) {LOGE("ERROR: MediaPlayer native registration failed\n");goto bail; } ? 就將此*.so的<函數名稱表>登記到VM里,以便能加快后續調用本地函數之效率。JNI_OnUnload()與JNI_OnLoad()? 當VM釋放該C模塊時,則會調用JNI_OnUnload()函數來進行善后清除動作。 registerNativeMethods()函數之用途? Java類別透過VM而調用到本地函數。 ? 一般是仰賴VM去尋找*.so里的本地函數。如果需要連續調用很多次,每次都需要尋找一遍,會多花許多時間。 ? 此時,將此*.so的<函數名稱表>登記到VM里。例如,在Android的/system/lib/libmedia_jni.so檔案里的程序碼片段如下:// #define LOG_NDEBUG 0 #define LOG_TAG "MediaPlayer-JNI" // ……… static JNINativeMethod gMethods[] = {{"setDataSource", "(Ljava/lang/String;)V",(void *)android_media_MediaPlayer_setDataSource},{"setDataSource", "(Ljava/io/FileDescriptor;JJ)V", (void *)android_media_MediaPlayer_setDataSourceFD},{"prepare", "()V", (void *)android_media_MediaPlayer_prepare},{"prepareAsync", "()V",(void *)android_media_MediaPlayer_prepareAsync},{“_start", "()V", (void *)android_media_MediaPlayer_start},{“_stop", "()V", (void *)android_media_MediaPlayer_stop},(省略) }; // ……… static int register_android_media_MediaPlayer(JNIEnv *env) {………return AndroidRuntime::registerNativeMethods(env,"android/media/MediaPlayer", gMethods, NELEM(gMethods)); } // ………. jint JNI_OnLoad(JavaVM* vm, void* reserved){………if (register_android_media_MediaPlayer(env) < 0) {LOGE("ERROR: MediaPlayer native registration failed\n");goto bail;}// ………. }? JNI_OnLoad()調用register_android_media_MediaPlayer()函數。 ? 此時,就調用到AndroidRuntime::registerNativeMethods()函數,向VM登記gMethods[]表格。 ? 登記gMethods[]表格的用途有二:1. 更有效率去找到C函數。2. 可在執行期間彈性進行抽換。 ? 由于gMethods[]是一個<名稱,函數指針對照表,在程序執行時,可多次調用registerNativeMethods()來更換本地函數之指針,而達到彈性抽換本地函數之目的。72 - JNI_從C調用Java函數a
1. Why? 將控制點下移到下C/C++層
? 茲想一想,當我們回家時,拿出手機來與門邊NFC Tag相互”親親”一下,手機就知道回家了,手機變靜音,畫面App都調整改變了,控制點在哪里呢? ? 在你寫的 Java層App子類? ? 在Android框架(基類)? ? 在你寫底層C/C++層模塊(含驅動*.so)?73 - JNI_從C調用Java函數b
2. 控制點與函數調用
? C調用Java函數,并不一定表示C層擁有控制點。 ? 但是,C層擁有控制點的必備表現是:C調用Java層函數? C/C++掌握主導權(話語權)、擁有控制點的更多表現: 除了C函數調用Java層函數之外,還有: 1. C函數存取Java對象的屬性值。 2. C函數創建Java層的對象(object)。74 - JNI_從C調用Java函數c
3. How-to:從C調用Java函數
? 如果控制點擺在本地C層,就會常常1. 從本地C函數去調用Java函數; 2. 從本地C函數去存取Java層對象的屬性值; 3. 從本地C函數去創建Java層的對象。從C調用Java函數
? 關于JNI,大家都知道如何從Java調用C函數。然而,在Android里,反而由C呼叫Java的情形才更具關鍵性。例如,Activity的跨進程溝通如下: ? 當App里的Activity透過IBinder接口來與Service進行IPC溝通時,事實上是由Java層的Activity調用C/C++模塊去進行IPC溝通,再由C模塊調用Java層的Service。 ? 所以,Java與C函數的雙向調用都是Android平臺的重要機制。
說明nativeSetup()函數的內容
? 上述的nativeSetup()函數之定義:JNIEXPORT void JNICALL Java_com_misoo_counter_CounterNative_nativeSetup(JNIEnv *env, jobject thiz) {//…………..}? 其中的第2個參數thiz就是Java層目前對象的參考(Reference)。所謂目前對象就是正在調用此本地函數的Java層對象。例如,在此范例里,就是CounterNative類的對象參考。 ? 指令:jclass clazz = (*env)->GetObjectClass(env, thiz);? 向VM(Virtual Machine)詢問這thiz所參考對象的類(即CounterNative類別)。 ? 由于這class是這本地函數的區域(Local)變量,當此函數執行完畢后,這個class變量及其所參考的值都會被刪除。因此,使用指令:m_class = (jclass)(*env)->NewGlobalRef(env, clazz); ? 來將區域型的class參考轉換為全域(Global)型的參考,并將此全域參考存入到這本地C模塊的全域變數m_class里。 ? 如此,當函數執行完畢后,這個m_class變量及其所參考的值都不會被刪除掉。同理,thiz也是區域變量,函數執行完畢,這個thiz及其值都會被刪除。因此,使用指令:m_object = (jobject)(*env)->NewGlobalRef(env, thiz); ? 將區域型的class參考轉換為全域(Global)型的參考,并將此全域參考存入到這本地C模塊的全域變數m_object里。 ? 接著,指令:m_mid_static = (*env)->GetStaticMethodID(env,m_class, "setValue", "(I)V"); ? 這要求VM去取得m_class所參考的類(就是CounterNative類)的setValue()函數的ID值。并將此ID值存入到這本地C模塊的全域變數m_mid_static里。 ? 同理,指令: m_mid = (*env)->GetMethodID(env, m_class,"setV", "(I)V"); ? 這找到CounterNative類的setV()函數的ID,并將此ID值存入到這本地C模塊的全域變數m_mid里。 ? 由于m_class和m_object兩者都是參考(Reference),其必須透過VM的NewGlobalRef()來轉換出全域性的參考。至于m_static_mid和m_mid則是一般的整數值,直接儲存于靜態變量里即可了。說明nativeExecute()和nativeExec()函數的內容
? 于此,這nativeSetup()函數已經將m_class、m_object、m_static_mid和m_mid儲存妥當了,準備好讓后續調用nativeExecute()和nativeExec()函數時能使用之。 說明nativeExecute()和nativeExec()函數的內容 ? 例如: JNIEXPORT void JNICALL Java_com_misoo_counter_CounterNative_nativeExecute(JNIEnv *env, jclass clazz, jint n){// …………(*env)->CallVoidMethod(env, m_object, m_mid, sum); }? 這個m_object正指向Java層的目前對象,而m_mid則是其setV()函數的ID。 ? 依據這兩項資料,就能透過VM的CallVoidMethod()函數而調用到目前Java對象的setV()函數,而把數據傳送到Java層。? 拿目前對象指針換取它的類(目前類)ID:jclass clazz = (*env)->GetObjectClass(env, thiz); ? 拿目前類ID換取某函數ID:m_mid = (*env)->GetMethodID(env, m_class, "setV","(I)V"); ? 依據類ID和函數ID,調用這指定的類里的指定的函數:(*env)->CallVoidMethod(env, m_object, m_mid, sum);75 - JNI_從C調用Java函數d
4. C函數存取Java對象的值
步驟: 0. 有了Java層對象(thiz) 1. 問這個對象thiz的類,得到clazzjclass clazz = (*env)->GetObjectClass(env, thiz); 2. 問這個類里的setV()函數,得到methodIDm_mid = (*env)->GetMethodID(env, clazz, "setV", "(I)V"); 3. 基于methodID和thiz,調用setV()函數int sum = 25;(*env)->CallVoidMethod(env, thiz, m_mid, sum); C函數直接存取屬性值? 剛才是透過函數調用(function call)來存取Java對象的屬性值。 ? C函數也能直接存取屬性值。步驟: 0. 有了Java層對象(thiz) 1. 問這個對象thiz的類,得到clazzjclass clazz = (*env)->GetObjectClass(env, thiz); 2. 問這個類里的numb屬性,得到fieldIDm_fid = (*env)->GetFieldID(env, clazz, "numb", "I"); 3. 基于fieldID和thiz,直接存取numb值n = (int)(*env)->GetObjectField(env, m_object, m_fid);范例 ? 例如,在CounterNative里可定義numb等屬性,如下: 拿目前對象換取它的類 拿此類換取某屬性ID 依據對象和屬性ID,取到屬性值 調用setV()函數,將sum回傳到java層 // ac01.java // ……… public class ac01 extends Activity implements OnClickListener {static public ac01 ref;@Overridepublic void onCreate(Bundle savedInstanceState){ref = this;// ………..}@Override public void onClick(View v) { switch(v.getId()){ case 101:CounterNative cn = new CounterNative();cn.nativeExec(); break; case 103:finish(); break;}}}? 指令:cn.nativeExec()。由于nativeExec()是個本地函數,就轉而調用到com_misoo_counter_CounterNative_nativeExec()函數。 ? 其先取得Java層的numb值,計算出sum值,再調用Java層的setV()函數,顯示出來。// CounterNative.java // ……… public class CounterNative {private static Handler h;private int numb;static { System.loadLibrary("MyCounter2"); }public CounterNative(){h = new Handler(){public void handleMessage(Message msg) {ac01.ref.setTitle(msg.obj.toString());}}; numb = 25; nativeSetup();}private void setV(int value){ String str = "Value = " + String.valueOf(value); Message m = h.obtainMessage(1, 1, 1, str); h.sendMessage(m);}private native void nativeSetup();public native void nativeExec(); } ? 由于本地的C函數仍屬于CounterNative類的一部分,所以C函數仍可以調用到CounterNative類的private函數(如setV()函數)。 ? 此外,本地函數nativeSetup()只提供給建構函數來調用,而不給其它類別使用,所以可以將nativeSetup()宣告為private函數。/* com.misoo.counter.CounterNative.c */ #include "com_misoo_counter_CounterNative.h" jobject m_object; jmethodID m_mid; jfieldID m_fid; JNIEXPORT void JNICALL Java_com_misoo_counter_CounterNative_nativeSetup(JNIEnv *env, jobject thiz) {jclass clazz = (*env)->GetObjectClass(env, thiz);m_object = (jobject)(*env)->NewGlobalRef(env, thiz);m_mid = (*env)->GetMethodID(env, clazz, "setV", "(I)V");m_fid = (*env)->GetFieldID(env, clazz, "numb", "I");return;}JNIEXPORT void JNICALL Java_com_misoo_counter_CounterNative_nativeExec(JNIEnv *env, jobject thiz) {int n, i, sum = 0;n = (int)(*env)->GetObjectField(env, m_object, m_fid);for(i=0; i<=n; i++)sum+=i;(*env)->CallVoidMethod(env, m_object, m_mid, sum);return;}? 其中的thiz就是從Java層傳遞過來的對象指針。首先將thiz傳給VM的GetObjectClass()函數,取得該對象的類指針(即clazz)。 ? 接著,將clazz傳給VM的GetFieldID()函數來取得numb屬性的ID。 ? 當ac01調用CounterNative類的nativeExec()本地函數時,就轉而調用C語言的nativeExec()函數。 ? 這個C函數調用VM的GetObjectField()函數,使用剛才取得的m_fid值,而取得CounterNative類的對象里的numb屬性值。76 - JNI_從C調用Java函數e
5. 從C創建Java對象
? 目前你已經會調用Java函數了。 ? 那就會調用一種特別的函數了。 ? 這種特別的函數,叫作構造式。 ? 調用構造式,就能創建對象了。 ? 在前面的范例里,都是先誕生Java層對象,然后將該對象的參考(Reference)傳遞給C模塊。 ? 本節的范例將改由C模塊來誕生Java層的對象。 ? 此例子,改由C模塊來誕生Java層的ResultValue對象,其意味著C模塊擁有較大的掌控權。 ? 也就是說,整個應用程序的控制中心點,從Java層轉移到本地的C模塊。 ? 如果你決定由C模塊來主導系統的執行,這項技巧是非常重要的。77 - JNI_從C調用Java函數f
創建與thiz同類的對象
1. 問這個對象thiz的類,得到clazz。 2. 問這個類里的<init>()構造式,得到methodID。 3. 基于methodID,調用構造式(創建對象) 。創建與thiz不同類的對象
1.問特定的類,得到clazz。jclass clazz = (*env)->FindClass(env,"com/misoo/counter/ResultValue"); 2. 問這個類里的<init>()構造式,得到methodID。jmethodID constr = (*env)->GetMethodID(env, clazz, “<init>", "()V"); 3. 基于methodID,調用構造式(創建對象) 。jobject ref = (*env)->NewObject(env, clazz, constr); ? 創建與Thiz同類的對象,對控制點的意義不大。因為Java層已經創建該類的對象,無法防止了。 ? 創建與Thiz不同類的對象,有很大的控制涵意。 ? 這CounterNative類別里定義了1個抽象函數,以及1個本地函數。 ? 抽象函數是由子類來實作;而本地函數則由C模塊來實作。如下: // CounterSub.java // ………… public class CounterSub extends CounterNative{ protected int getN(){ return 10; } }// ac01.java // ………. public class ac01 extends Activity implements OnClickListener {private CounterNative cn;@Overridepublic void onCreate(Bundle savedInstanceState){// …………cn = new CounterSub(); }@Override public void onClick(View v) { switch(v.getId()){case 101: ResultValue rvObj= (ResultValue)actNative.nativeExec();setTitle("Value = " + rvObj.getValue()); break; case 103: finish(); break; }}}cn = new CounterSub() ? 就調用CounterSub子類別的建構函數,其調用CounterNative()父類別的建構函數。 ? 此刻,先調用子類別的getN()函數,取得numb屬性值。 接著調用本地的C函數 // CounterNative.java // …….. abstract public class CounterNative {private int numb;static {System.loadLibrary("MyCounter5"); }public CounterNative(){numb = getN();nativeSetup();}abstract protected int getN();private native void nativeSetup(); } ? 透過VM而調用ResultValue類的建構函數去誕生ResultValue對象。/* com.misoo.counter.Counter.c */ #include <android/log.h> #include "com_misoo_counter_actNative.h" #include "com_misoo_counter_CounterNative.h" jobject m_object, m_rv_object ; jfieldID m_fid; jmethodID m_rv_mid;JNIEXPORT void JNICALL Java_com_misoo_counter_CounterNative_nativeSetup(JNIEnv *env, jobject thiz) {jclass clazz = (*env)->GetObjectClass(env, thiz);m_object = (jobject)(*env)->NewGlobalRef(env, thiz);m_fid = (*env)->GetFieldID(env, clazz, "numb", "I");jclass rvClazz = (*env)->FindClass(env,"com/misoo/counter/ResultValue");jmethodID constr =(*env)->GetMethodID(env, rvClazz, "<init>", "()V");jobject ref = (*env)->NewObject(env, rvClazz, constr);m_rv_object = (jobject)(*env)->NewGlobalRef(env, ref);m_rv_mid= (*env)->GetMethodID(env, rvClazz, "setV", "(I)V");return; }JNIEXPORT jobject JNICALL Java_com_misoo_counter_actNative_nativeExec(JNIEnv *env, jclass clazz) {int n, i, sum = 0;n = (int)(*env)->GetObjectField(env, m_object, m_fid);for(i=0; i<=n; i++)sum+=i;(*env)->CallVoidMethod(env, m_rv_object, m_rv_mid, sum);return m_rv_object; }? 上述nativeSetup()函數里的指令:jclass rvClazz = (*env)->FindClass(env,"com/misoo/counter/ResultValue"); ? 直接把"com/misoo/counter/ResultValue" 字符串寫進去,VM的就可幫忙找到ResultValue類的參考(存于rvClazz內)。 ? 執行指令: jmethodID constr = (*env)->GetMethodID(env,rvClazz, "<init>", "()V"); ? <init> 符號就代表構造式,VM的GetMethodID()函數取得構造式的ID,存于constr內。 ? 執行到指令:jobject ref = (*env)->NewObject(env, rvClazz, constr); ? 此時rvClazz代表ResultVlaue類,而constr是ResultValue類的構造式。 ? 于是,以rvClazz和constr兩者為參數,調用VM的NewObject()函數,誕生一個ResultVlaue對象了。 ? NewObject()誕生ResultVaue對象后,會將該對象參考回傳給C模塊。 ? 由于Java層并沒有這新對象的參考,所以此刻nativeExce()函數里的指令:return m_rv_object; ? 就將新對象參考傳遞給Java層,讓ac01類別能順利讀取對象里的數據。// ResultValue.java // …….. public class ResultValue {private int mValue;private void setV(int value){ mValue = value; }public int getValue(){ return mValue; } }// ac01.java // ………. public class ac01 extends Activity implements OnClickListener { @Override public void onClick(View v) {// …….. switch(v.getId()){case 101: ResultValue rvObj= (ResultValue) actNative.nativeExec();setTitle("Value = " + rvObj.getValue()); break; case 103: finish(); break; }}} // actNative.java // …… public class actNative { public static native Object nativeExec(); }JNIEXPORT jobject JNICALL Java_com_misoo_counter_actNative_nativeExec(JNIEnv *env, jclass clazz) {結語: ? 由于Android是開源開放的平臺,我們才有將控制點往下,移到C/C++層的機會。 ? 當你使用手機時,你所摸的都是硬件,例如觸摸屏、鍵盤等。 ? 你從來沒有摸過軟件,信不信,不然你說說軟見摸起來感覺如何? 摸起來像貓咪? 像海綿? ? 因此C/C++層代碼比Java層代碼更早偵測到用戶的事件,所以控制點往下移到C/C++層有效促進軟硬整合,讓硬件的創新迅速浮現出來,與Java層App代碼緊密結合。78 - JNI_有必要的優化設計a
1. 創建C++類的對象
? 在JNI的C模塊里,不僅能創建Java層的對象,也可以創建C++類別的對象,如下圖: ? 上圖的JNI接口層是以C語言實作的本地函數。 ? 在邏輯上,這些C函數仍屬于Java類(即定義<In>的類) 。 ? 典型的架構共分為三個層級:Java層、C層和C++層;其間可以互相溝通與合作。 ? C和C++代碼可以擺在同一個*.so檔案里。 ? 多個Java類的C函數(即多個<In>的實現代碼)可以擺在同一個*.so檔案里。2. 優化目的:維護本地函數的穩定性
? 不宜仰賴C層的*.so的全局變量來儲存Java層或C++層的對像(指針或參考)。 ? 依賴C層(全局或靜態變量)來儲存C++對象指針,或者儲存Java層對象參考,這常常讓C層模塊與特定C++對象或Java對象綁在一起,產生緊密的相依性,導致系統彈性的下降。 ? 本節的范例將以優越的設計化解這項困境。 議題 ? 由于ResultValue對象是在run-time時期動態創建的,如果有多個對象時,該如何儲存呢? ? 如果多個Java線程并行地(Concurrently)執行這個本地函數,共享了m_object和m_rv_object變量,如何確保線程之間不互相沖突呢?不將java或c++對象參考存儲于C層的全局變量里,提升C函數和代碼穩定性? C層的全局或靜態(static)變量只適合儲存靜態的數據,例如methodID或fieldID值。 ? 這m_fid儲存的是類的屬性ID,靜態對靜態關系,是合理的。 ? Java層的每一個CounterNative類的對象來調用本地NativeSetup()時,都可利用m_fid值來取得各對象里的numb屬性值(無論有多少個Java層的CounterNative對象)。79 - JNI_有必要的優化設計b
3. <靜態對靜態,動態對動態>原則
? 在JNI的C模塊里,不僅能創建Java層的對象,也可以創建C++類的對象。 ? 但是,要將CCounter類新對象的指針放在那里才合理呢? ? 這nativeSetup()函數動態創建CCounter類的對象,并將新對象的指針儲存于全局(靜態)的mObject變量里。 ? 接著,nativeExec()函數透過mObject變量的指針值而調用CCounter的execute()函數。/* com.misoo.counter.CounterNative.cpp */ #include "com_misoo_counter_actNative.h" #include "com_misoo_counter_CounterNative.h“ class CCounter {int n; public:CCounter(int v) { n = v; }int execute(){ int i, sum = 0;for(i=0; i<=n; i++) sum+=i;return sum; } } *mObject;JNIEXPORT void JNICALL Java_com_misoo_counter_CounterNative_nativeSetup(JNIEnv *env, jobject thiz) {mObject = new CCounter(10);} JNIEXPORT jint JNICALL Java_com_misoo_counter_actNative_nativeExec(JNIEnv *env, jclass clazz) {return (jint) mObject->execute(); }80 - JNI_有必要的優化設計c
4. Java與C++對象之間的<單向>對稱關連
// CounterNative.java // …….. public class CounterNative { private int mObject; static {System.loadLibrary("MyCounter7");} public CounterNative(int numb) {nativeSetup( numb );} private native void nativeSetup(int n); }? 當你定義C++類別時,可以將它與JNI的C函數定義在同一個文件(*.so)里,也可定義在獨立的檔案里。 ? 在此范例里,在JNI的C函數文件中,新增一個CCounter類。 ? 例如,在com_misoo_counter_CounterNative.cpp里除了實作本地C函數之外,還定義了一個C++的CCounter類。/* com.misoo.counter.CounterNative.cpp */ #include "com_misoo_counter_actNative.h" #include "com_misoo_counter_CounterNative.h“ class CCounter{int n;public:CCounter(int v) { n = v; }int execute() {int i, sum = 0;for(i=0; i<=n; i++) sum+=i;return sum;}};JNIEXPORT void JNICALL Java_com_misoo_counter_CounterNative_nativeSetup(JNIEnv *env, jobject thiz, jint n) {CCounter *obj = new CCounter(n);jclass clazz = (jclass)env->GetObjectClass(thiz);jfieldID fid = (jfieldID)env->GetFieldID(clazz, "mObject", "I");env->SetIntField(thiz, fid, (jint)obj); }JNIEXPORT jint JNICALL Java_com_misoo_counter_actNative_nativeExec(JNIEnv *env, jclass clazz, jobject obj) {jclass objClazz = (jclass)env->GetObjectClass(obj);jfieldID fid = env->GetFieldID(objClazz, "mObject", "I");jlong p = (jlong)env->GetObjectField(obj, fid);CCounter *co = (CCounter*)p;return (jint)co->execute(); } ? 上述nativeSetup()函數里的指令:CCounter *obj = new CCounter(n); ? 創建一個C++層的CCounter對象,并且把n值存入其中。 ? 隨后,指令: jclass clazz = (jclass)env->GetObjectClass(thiz); jfieldID fid = (jfieldID)env->GetFieldID(clazz, "mObject", "I"); ? 取得該CCounter對象的mObject屬性ID。 ? 接著,指令:env->SetIntField(thiz, fid, (jint)obj); ? 就將CCounter對象的指針值儲存于CounterNative對象的mObject屬性里,如此建立了CounterNative對象與CCounter對象之連結。 ? C模塊創建CCounter對象之后,立即將CCounter對象指針儲存于CounterNative的mObject屬性里。靜態對靜態,動態對動態? C模塊來創建C++對象,然后讓Java對象與C++對象之間產生成雙成對的連結關系。 ? C模塊本身并不儲存Java或C++對象的指針或參考值。而是僅負責創建C++對象,并建立Java與C++的對象間的連結關系。 ? 如此,C模塊能替眾多Java對象服務,而不再與特定的Java對象綁在一起了。 ? 一旦解開C模塊與C++對象(或Java對象)之間的相依性,C模塊就能具有通用性。 ? 例如,C層nativeSetup()函數,能為Java層的每一個對象建立其相對映的C++對象。 ? 由于C層的nativeSetup()已經變成為通用型的函數了,每次調用它時,只要將特定的CounterNative對象傳遞給它,就能順利找到其相對映的CCounter對象了。如下圖: // actNative.java // ……… public class actNative { public static native int nativeExec(Object obj); }? 這nativeExec()先取得CounterNative對象里的mObject屬性值,相當于取得CCounter對象的指針了,就能調用CCounter對象的execute()函數了。 ? 由于C模塊里并沒有儲存CounterNative對象的指針,所以Java必須將CounterNative對象的參考值傳遞給JNI層的nativeExec()本地函數,如下指令:? 編修ac01.java類別:// ac01.java // …….. public class ac01 extends Activity implements OnClickListener {private CounterNative cn1, cn2;@Override public void onCreate(Bundle savedInstanceState){//……..cn1 = new CounterNative(10);cn2 = new CounterNative(12);}? ac01.java類:@Override public void onClick(View v) { int sum; switch(v.getId()){ case 101: sum = actNative.nativeExec(cn1);setTitle("Sum = " + sum);break; case 102: sum = actNative.nativeExec(cn2);setTitle("Sum = " + sum);break; case 103: finish();break; }}}? 指令: actNative.nativeExec( cn1 ); ? 此時,ac01將CounterNative類別的第1個對象傳遞給JNI模塊的nativeExec()函數,找到相對映的CCounter對象,然后調用它的execute()函數。JNIEXPORT jint JNICALL Java_com_misoo_counter_actNative_nativeExec(JNIEnv *env, jclass clazz, jobject obj) {jclass objClazz = (jclass)env->GetObjectClass(obj);jfieldID fid = env->GetFieldID(objClazz, "mObject", "I");jlong p = (jlong)env->GetObjectField(obj, fid);CCounter *co = (CCounter*)p;return (jint)co->execute(); }? 這obj參考到CounterNative對象。 ? 當其執行到指令:jfieldID fid = env->GetFieldID(objClazz, "mObject", "I");jlong p = (jlong)env->GetObjectField(obj, fid); ? 就從CounterNative對象里取得mObject屬性值,并存入p變量里。此p值正是C++層CCounter對象的參考,所以可透過p調用CCounter對象的execute()函數。81 - JNI_有必要的優化設計d
5. Java與C++對象之間的<雙向>對稱關連
舉例說明 ? 上一節里,將C++對象指針儲存于Java對象的屬性里;成為<單向>的對稱聯結關系。 ? 接下來,也可以將Java對象的參考儲存于C++對象里。 // INumber.java package com.misoo.counter; public interface INumber {int onNumber(); }// ac01.java // ……… public class ac01 extends Activityimplements OnClickListener, INumber {private CounterNative cn;@Override public void onCreate(Bundle savedInstanceState){//………….-cn = new CounterNative();cn.setOnNumber(this);}@Override public void onClick(View v) { int sum; switch(v.getId()){ case 101:sum = actNative.nativeExec(cn.mObject);setTitle("Sum = " + sum);break; case 103:finish(); break;} } @Override public int onNumber() { return 17; } }? 指令: @Override public void onCreate(Bundle savedInstanceState){//………….-cn = new CounterNative();cn.setOnNumber(this);} // CounterNative.java package com.misoo.counter; public class CounterNative { public int mObject; private INumber listener;static {System.loadLibrary("MyCounter8"); } public CounterNative(){ nativeSetup(); } public void setOnNumber(INumber plis){ listener = plis; } private int getNumb(){ return listener.onNumber(); } private native void nativeSetup(); }/* com.misoo.counter.CounterNative.cpp */ #include "com_misoo_counter_actNative.h" #include "com_misoo_counter_CounterNative.h“ class CCounter{public:int n;jint javaObj;public:CCounter() {}int execute() {int i, sum = 0;for(i=0; i<=n; i++) sum+=i;return sum;} };JNIEXPORT void JNICALL Java_com_misoo_counter_CounterNative_nativeSetup(JNIEnv *env, jobject thiz) {CCounter *obj = new CCounter();jclass clazz = (jclass)env->GetObjectClass(thiz);jfieldID fid =(jfieldID)env->GetFieldID(clazz, "mObject", "I");env->SetIntField(thiz, fid, (jint)obj);jobject gThiz = (jobject)env->NewGlobalRef(thiz);obj->javaObj = (jint)gThiz; }JNIEXPORT jint JNICALL Java_com_misoo_counter_actNative_nativeExec(JNIEnv *env, jclass clazz, jint refer) {CCounter *co = (CCounter*)refer;jobject jo = (jobject)co->javaObj;jclass joClazz = (jclass)env->GetObjectClass(jo);jmethodID mid = env->GetMethodID(joClazz,"getNumb", "()I");int numb = (int)env->CallIntMethod(jo, mid);co->n = numb;return (jint)co->execute(); }關于nativeSetup()函數的動作? 上述nativeSetup()函數里的指令:CCounter *obj = new CCounter(); 誕生一個CCounter對象。 ? 指令: 關于nativeSetup()函數的動作jfieldID fid = (jfieldID)env->GetFieldID(clazz,"mObject", "I");env->SetIntField(thiz, fid, (jint)obj); ? 就將CCounter對象的指針值儲存于CounterNative對象的mObject屬性里。 ? 指令:jobject gThiz = (jobject)env->NewGlobalRef(thiz);obj->javaObj = (jint)gThiz; ? 就將CounterNative對象的指針值儲存于CCounter對象的javaObj屬性里,如此建立了CounterNative對象與CCounter對象之雙向連結。如下圖: 關于nativeExec()函數的動作? 當ac01調用這個函數時,將CCounter對象的參考值傳遞給JNI層的nativeExec()本地函數。 // ac01.java @Override public void onClick(View v) { int sum; switch(v.getId()){ case 101:sum = actNative.nativeExec( cn.mObject );setTitle("Sum = " + sum);break; case 103:finish(); break;} } @Override public int onNumber() { return 17; } }// actNative.java package com.misoo.counter; public class actNative { public static native int getCounter(int refer); public static native int nativeExec(int refer); }? 指令:actNative.nativeExec(cn.mObject); ? 這nativeExec()函數的參數refer則參考到CCounter的對象。當其執行到指令:CCounter *co = (CCounter*)refer;jobject jo = (jobject)co->javaObj; ? 就從CCounter對象里取得javaObj屬性值,并存入jo變量里。 ? 此jo值正是Java層CounterNative對象的參考,所以透過jo可以調用CounterNative對象的getNumb()函數,進而調用ac01的onNumber()函數,順利取得n值(即numb值)。 ? 最后,指令:co->n = numb;return (jint)co->execute(); ? 將取到的numb值存入CCounter對象里,并調用其execute()函數算出結果,回傳給Java層。82 - JNI_有必要的優化設計e
結語 ? 在此范例里,ac01類和actNative開發者知道CounterNative類的內涵,而且CounterNative的mObject必須public,才能取得co的指針。 // ac01.java // ……… public class ac01 extends Activityimplements OnClickListener, INumber {private CounterNative cn;@Override public void onCreate(Bundle savedInstanceState){//………….-cn = new CounterNative();cn.setOnNumber(this);} @Override public void onClick(View v) { int sum; switch(v.getId()){ case 101:sum = actNative.nativeExec( cn.mObject );setTitle("Sum = " + sum);break; case 103:finish(); break;} } @Override public int onNumber() { return 17; } }// CounterNative.java package com.misoo.counter; public class CounterNative { public int mObject; private INumber listener;static {System.loadLibrary("MyCounter8"); } public CounterNative(){ nativeSetup(); } public void setOnNumber(INumber plis){ listener = plis; } private int getNumb(){ return listener.onNumber(); } private native void nativeSetup(); } ? 如果CouterNative的mObject屬性改為private時,ac01或actNative就拿不到mObject屬性值了。 ? 只能將cn指針傳遞給C函數。 ? 此時,必須有個前置的預備動作(setup)了。 // ac01.java// ……… @Override public void onClick(View v) { int sum; switch(v.getId()){ case 101:sum = actNative.nativeExec( cn );setTitle("Sum = " + sum);break; case 103:finish(); break;} } @Override public int onNumber() { return 17; } } 靜態對靜態 動態對動態83 - 多個Java純種進入本地函數a
1. 介紹JNI線程模式
Android線程的特性
? 線程(Thread)又稱為「執行緒」。 ? 于默認(Default)下,一個App的各類別(如Activity、BroadcastReceiver等)都在同一個進程(Process)里執行,而且由該進程的主線程負責執行。 ? 如果有特別指示,也可以讓特定類在不同的進程里執行。Android線程的特性 ? 例如由一個Activity啟動一個Service,在默認情形下,兩者都在同一個進程里執行。 ? 主線程除了要處理Activity類別的UI事件,又要處理Service幕后服務工作,通常會忙不過來。 ? 該如何化解這種困境呢? ? 主線程可以誕生多個子線程來分擔其工作,尤其是比較冗長費時的幕后服務工作,例如播放動畫的背景音樂、或從網絡下載映片等。 ? 于是,主線程就能專心于處理UI畫面的事件了。線程往返Java與C/C++
? 由于每一個進程里都有一個主線程。 ? 每一個進程里,都可能有Java程序碼,也有C/C++本地程序碼。 ? Java層的主線程經常從Java層進入JNI層的C函數里執行;此外,當反向調用Java函數時,又返回進入Java函數里執行。? 無論是主線程,或是子線程,都可以從Java層進入C/C++層去執行,也能從C/C++層進入Java層。 ? 在本節里,就來說明跨越JNI的線程模式,以及如何化解線程的沖突問題。VM對象與JavaVM指針
? 在進程里,有一個虛擬機(Virtual Machine,簡稱VM)的對象,可執行Java代碼,也引導JNI本地程序的執行,實現Java與C/C++之間的溝通。? 當VM執行到System.loadLibrary()函數去加載C模塊時會時,就會立即先調用JNI_OnLoad()函數。 ? VM調用JNI_OnLoad()時,會將VM的指標(Pointer)傳遞給它,其參數如下:/* com.misoo.counter.CounterNative.cpp */ // ……… JavaVM *jvm; // ……… jint JNI_OnLoad( JavaVM* vm, void* reserved){jvm = vm;return JNI_VERSION_1_4; }? 指令:jvm = vm;將傳來的VM指針儲存于這本地模塊(*.so)的公用變量jvm里。讓本地函數隨時能使用jvm來與VM交互。 ? 例如,當你創建一個本地C層的新線程時,可以使用指令:jvm->AttachCurrentThread(&env, NULL); ? 就向VM登記,要求VM誕生JNIEnv對象,并將其指針值存入env里。有了env值,就能執行指令:env->CallStaticVoidMethod(mClass, mid, sum); ? 其調用Java層的函數了。為什么需要JNIEnv對象呢?
? 本地C函數的第1個參數就是JNIEnv對象的指針,例如: ? 這是Java線程透過VM進入C函數時,VM替線程而創建的對象,是該線程專屬的私有對象。 ? 線程透過它來要求VM協助進入Java層去取得Java層的資源,包括:取得函數或屬性ID、調用Java函數或存取Java對象屬性值等。 ? 例如,有了env值,就能執行指令:env->CallStaticVoidMethod(mClass, mid, sum); ? 其調用Java層的函數了。議題:? 在C/C++層所創建的子線程,沒有經過VM,所以沒有JNIEnv對象,該如何要求VM協助進入Java層去取得Java層的資源,例如取得函數ID、調用Java函數呢?? 使用指令:jvm->AttachCurrentThread(&env, NULL); ? 就向VM登記,要求VM誕生JNIEnv對象,并將指針存入env里。有了env值,就能執行指令:env->CallStaticVoidMethod(mClass, mid, sum); ? 其調用Java層的函數了。84 - 多個Java純種進入本地函數b
2. 從Session概念認識JNIEnv對象
? 只要你寫過WebService應用,你就會有Session(對象)的概念。 ? 由于Client 與 Server是 N : 1關系所以Server替每一個Client的Connection準備一個Session對象,讓各Connection使用自己專屬的對象,避免共享對象的數據安全問題。 ? 在這Client與Server之間透過接口互相溝通;而且多個 Client可同時與Server建立連結,取得Server的服務。所以,Client與Server之間是N:1的關系,如下圖: ? 基于這個架構,可以建立Client與Server之間的各種連結(Connection)和溝通(Communication)。 ? 例如,Client端的瀏覽器(Browser)會與Server建立連結,然后開起一段交談(Session)。? 首先,Client透過某項機制(例如,呼叫公用的getConnection()函數等)來建立與Server之間的連結,此時Server就把它的接口(即IServer)回傳給Client,如下圖: ? 透過剛才所建立的連結關系,Client就能呼叫Server的getSession()函數,準備開啟一段對話。 ? 此時,Server就誕生一個Session對象,來作為這項連結的專屬對象,可以記載對話過程所產生的信息。 ? 把ISession口回傳給Client,讓Client直接與Session溝通;才間接與Server溝通。如下圖: ? Client掌握了ISession接口,就能透過ISession接口來呼叫Session的函數,然后由Session 來與Server溝通。如下圖: 每一個connection都有一個私有的session對象 每一個線程進入VM都有一個私有JNIEnv對象85 - 多個Java純種進入本地函數c
3. 細說JNIEnv對象
舉例說明? 主、子執行緒都能進入JNI層的C函數,又反過來進入Java層去調用Java函數。 ? 按下<main thread>,主線程先進入C層去執行nativeSetup()函數,完畢后返回ac01。 ? 再進入C層去執行nativeExec()函數,隨后線程進入Java層的setV()函數。 ? 按下<sub thread>,主線程就誕生一個子線程去執行Task的run()函數,然后依循剛才主線程的路徑走一遍。 // actNative.java public class actNative { public static native void nativeExec(); }// CounterNative.java abstract public class CounterNative {private int numb;public ResultValue rvObj;static { System.loadLibrary("MyJT001"); }public CounterNative(){rvObj = new ResultValue();numb = getN();nativeSetup( rvObj );}abstract protected int getN();private native void nativeSetup( Object obj );}// ResultValue.java public class ResultValue {private int mValue;private String mThreadName;public int getValue(){ return mValue; }private void setV(int value){mValue = value;}}/* com.misoo.counter.CounterNative.c */ // …….. jobject m_object, m_rv_object; jfieldID m_fid; jmethodID m_rv_mid; JNIEXPORT void JNICALL Java_com_misoo_counter_CounterNative_nativeSetup (JNIEnv *env, jobject thiz, jobject refer) {jclass clazz = (*env)->GetObjectClass(env, thiz);m_object = (jobject)(*env)->NewGlobalRef(env, thiz);m_fid = (*env)->GetFieldID(env, clazz, "numb", "I");jclass rvClazz = (*env)->GetObjectClass(env, refer);m_rv_object = (jobject)(*env)->NewGlobalRef(env, refer);m_rv_mid = (*env)->GetMethodID(env, rvClazz, "setV", "(I)V"); }JNIEXPORT void JNICALL Java_com_misoo_counter_actNative_nativeExec (JNIEnv *env, jclass clazz){int n, i, sum = 0;n = (int)(*env)->GetObjectField(env, m_object, m_fid);for(i=0; i<=n; i++) sum+=i;(*env)->CallVoidMethod(env, m_rv_object, m_rv_mid, sum); }? 當Java 層的主線程準備進來執行這nativeSetup()函數時,VM就會誕生一個JNIEnv類別(或C結構)的對象,這個對象專屬于主線程。 ? 接著,將該對象的指針傳遞給nativeSetup()函數的第1個參數,如下:JNIEXPORT void JNICALL Java_com_misoo_counter_CounterNative_nativeSetup(JNIEnv *env, ……) {// ………}? 不僅僅針對主線程而已,VM也替其它線程創建JNIEnv對象,也在該線程進入JNI層C函數時將其指針傳遞給第1個參數。 ? 因此,不同的線程進入到nativeSetup()函數時,其所帶進來的env參數值都是不一樣的。 ? 這樣安排的好處之一是:每一個線程都不共享JNIEnv對象,此對象可以儲存該線程相關的數據值,如此可以避免線程因共享對象或數據而引發的線程沖突問題,已就是有效提升了JNI環境下的多線程的安全性。 ? JNIEnv對象內含一個指針,正指向VM的函數表(Function Table)。 ? 每一個線程第一次進入VM調用本地函數時,VM會替它誕生一個相對映的JNIEnv對象。 ? Java層的線程調用C層的本地函數時,該線程必然經過VM,且VM一定替它誕生相對映的JNIEnv對象。 ? 所以一個線程每次調用本地函數時,都會將其對映的JNIEnv對象指針值傳遞給本地函數。 ? 每一個線程都有其專屬的JNIEnv對象,所以不同的線程(例如th1和th2)調用同一個本地函數(例如f1(JNIEnv* env, …..)函數)時,這本地函數所接到的env值是不一樣的。線程不共享JNIEnv對象,成為"單線程"開發,不必煩惱線程安全問題,讓本地函數的撰寫單純化? 在預設情形下,在某個線程第一次進入VM去執行JNI層C函數時,VM就會替它誕生專屬的JNIEnv對象。只要該線程還存在著,就會一直保留它所專屬的JNIEnv對象。 ? 一個線程經常會多次進入VM去執行JNI層C函數,其中,每一次進入時,VM都會將其專屬的JNIEnv對象指針傳遞給C函數的第1個參數(即env)。 ? 因此,同一個線程每回進入C函數時,所帶進來的env參數值都是相同的。如下圖: ? 由于某個線程(如子線程SubTh#1)先后執行nativeSetup()和nativeExec()兩個函數,其帶進來的env指標值都相同,其都指向同一個JNIEnv對象(即該線程專屬的對象),因此在兩個函數里皆可以透過env指針而去取得該對象里的數據值,因而達成共享數據的目的。 ? 采取JNIEnv機制,既能避免多線程的相互沖突,還能達成跨函數的數據共享。// CounterSub.java package com.misoo.pk01; import com.misoo.counter.CounterNative; public class CounterSub extends CounterNative{ protected int getN() { return 15; } } // CounterSub22.java package com.misoo.pk01; import com.misoo.counter.CounterNative; public class CounterSub22 extends CounterNative{ protected int getN() { return 10; } }// ac01.java // …….. public class ac01 extends Activityimplements OnClickListener {private Thread t;private static Handler h;@Overridepublic void onCreate(Bundle savedInstanceState){//………h = new Handler(){public void handleMessage(Message msg) {setTitle("Value = " + cn2.rvObj.getValue());}}; } @Override public void onClick(View v) { switch(v.getId()){ case 101:cn1 = new CounterSub();actNative.nativeExec();setTitle("Value = " + cn1.rvObj.getValue());break; case 102:t = new Thread(new Task());t.start(); break; case 103: finish(); break; }} class Task implements Runnable { public void run() {cn2 = new CounterSub22();actNative.nativeExec();h.sendEmptyMessage(MODE_PRIVATE);}}}86 - 多個Java純種進入本地函數d
化解沖突的范例
? 解決途徑之一是:多個Java線程之同步(Synchronization)兩個線程(并行)執行execute()函數// JTX04.java // ……… public class JTX04 {……… public long calculate(){Thread t1 = new Thread(){public void run() {JTX04.this.execute(JTX04.this);}};t1.start();try { Thread.sleep(2000);} catch (InterruptedException e) { e.printStackTrace(); }String ss = execute(this);ac01.ref.setTitle("ss: " + ss);return 0;}……….private native void Init(Object weak_this);private native String execute( Object oSync ); }看誰先搶到這個對象的鑰匙key/* com_misoo_thread_JTX04.cpp */ // …….. JavaVM *gJavaVM; jmethodID mid; jclass mClass; // Reference to JTX04 class jobject mObject; // Weak ref to JTX04 Java object to call on char sTid[20]; unsigned int e1; int x; int sum; long test; JNIEXPORT jstring JNICALL Java_com_misoo_thread_JTX04_execute(JNIEnv *env, jobject thiz,jobject syncObj){env->MonitorEnter( syncObj );sum = 0;for(int i = 0; i<=10; i++) {sum += i; Thread_sleep(1);}env->CallStaticVoidMethod(mClass, mid, sum, 666);env->MonitorExit( syncObj );long pid = getpid();sprintf(sTid, "%lu", test);jstring ret = env->NewStringUTF(sTid);return ret; } // ……… }? 執行到指令: JTX04.this.execute( JTX04.this ); 和 String ss = execute( this ); ? 都把目前的Java 對象(即JTX04對象)傳遞給本地的execute()函數。? 先進入execute()的線程先執行到指令:env->MonitorEnter( syncObj ); ? 也就向JTX04對象索取鑰匙(Key)。由于JTX04對象只要一把鑰匙,所以其它后進入的線程只好停下來等待。? 當執行到指令:env->MonitorExit( syncObj ); ? 也就把鑰匙(Key)交還給JTX04對象,讓等待中的其它線程可以逐一進入。87 - 本地線程進入Java層a
1. 如何誕生Native層的子線程? 1. 如何創建本地的子線程?
? 在之前的范例里,線程都是在Java層創建的。 ? 如何在C層里創建新線程,并讓其進入Java層呢? ? 由于在創建C層新線程時,VM尚不知道它的存在,沒有替它創建專屬的JNIEnv對象,無法調用到Java層函數。 ? 此時,可以向VM登記而取得JNIEnv對象后,此線程就能進入Java層了。在C函數里創建子線程
// ……… pthread_t thread; void* trRun( void* ); JNIEXPORT jstring JNICALL Java_com_misoo_thread_JTX05_execute(JNIEnv *env, jobject thiz, jobject syncObj){………int th1 = pthread_create( &thread, NULL, trRun, NULL);……… } void* trRun( void* ) {// ……… }? 使用pthread_create()函數來創建本地的子線程。88 - 本地線程進入Java層b
2. Native線程進入Java層 先取得JNIEnv對象
復習 ? C層新線程沒有JNIEnv對象,無法調用到Java層函數。可以向VM登記而取得JNIEnv對象后,此線程就能進入Java層了。
89 - 本地線程進入Java層c
3. Native多線程的安全
? 使用函數pthread_create()函數來誕生Native層的子線程。 ? 由于Native函數里執行的線程也能誕生子線程,所以也應該注意其線程安全問題。例如:/* com_misoo_thread_JTX07.cpp */ // …….. JavaVM *gJavaVM; int sum; pthread_t thread; void* trRun( void* ); void callBack(JNIEnv *); jobject mSyncObj; //-------------------------------------------------------------- void Thread_sleep(int t){timespec ts; ts.tv_sec = t;ts.tv_nsec = 0; nanosleep(&ts, NULL);return; }void JNICALL Java_com_misoo_counter_CounterNative_nativeSetup(JNIEnv *env, jobject thiz, jobject weak_this){jclass clazz = env->GetObjectClass(thiz);mClass = (jclass)env->NewGlobalRef(clazz);mObject = env->NewGlobalRef(weak_this);mid = env->GetStaticMethodID(mClass, "callback","(II)V");return; }jstring JNICALL Java_com_misoo_counter_CounterNative_nativeExec(JNIEnv *env, jobject thiz, jobject syncObj){mSyncObj = env->NewGlobalRef(syncObj);int t1 = pthread_create( &thread, NULL, trRun, NULL);Thread_sleep(4);callBack(env); // m.t.//-----------------------------------------------------------long pid = getpid();sprintf(sTid, "%lu", pid);jstring ret = env->NewStringUTF(sTid);return ret; } //-------------------------------------------------------------------- void callBack(JNIEnv *env){env->MonitorEnter(mSyncObj);sum = 0;for(int i = 0; i<=10; i++) {sum += i;Thread_sleep(1);}env->CallStaticVoidMethod(mClass, mid, sum, 666);env->MonitorExit(mSyncObj); }void* trRun( void* ){int status;JNIEnv *env;bool isAttached = false;Thread_sleep(1);status = gJavaVM->GetEnv((void **) &env, JNI_VERSION_1_4);if(status < 0) {status = gJavaVM->AttachCurrentThread(&env, NULL);if(status < 0) return NULL;isAttached = true;}callBack(env); // t1if(isAttached) gJavaVM->DetachCurrentThread();return NULL; }? 主線程誕生了子線程去執行trRun()函數。必須先調用gJavaVM->AttachCurrentThread(&env, NULL); ? 才能取得子線程自己所屬的JNIEnv對象之參考了,并且調用Callback()函數。 ? 之后,主線程也調用同一Callback函數。 ? 于是,在Callback()函數里,使用env->MonitorEnter()和env->MonitorExit(mSyncObj);指令來讓各線程能達到同步。總結
以上是生活随笔為你收集整理的Android从程序员到架构师之路3的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Apache Hudi的编译及安装
- 下一篇: 单独使用Quartz 2.1.7 时Jo