2016年7月25日 星期一

Fragment畫面轉向、View.post與getActivity() 聯手演出的問題

問題

某Fragment透過View.post() 待View建立完成後執行相關動作,例如常見的取得View大小,但若post出去的Runnable物件內恰巧包含了getActivity()的相關用法,可能會遇上傳回值為null的情況。

Fragment於Activity.onCreate() 新建並替換現有畫面,在畫面轉向時隨Activity銷毀與重建,但一再新建的Fragment會取代舊有(自動重建)的Fragment,使其與Activity的連結中斷,但已post至主執行緒MessageQueue中的待執行任務仍會被執行,若其中的操作基於getActivity(),將發生Null Point Exception。

這個問題個人不常遇到,但感到挺有趣的,所以花點時間研究一下,若有謬誤麻煩告訴我,有什麼看法也歡迎討論!

還原現場

在MainActivity的onCreate()新建Fragment物件,為求簡便直接以replace方法將畫面置換。

MainActivity.java
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    …
    Fragment fraMain = new FragmentMain();
    getFragmentManager().beginTransaction().replace(R.id.FrameLayoutContent, fraMain).commit();
    …
}

在Fragment的onActivityCreated事件中載入TextView,並以Runnable包裹待View建立完成後要執行的動作,動作內包含getActivity()的呼叫。

FragmentMain.java
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    TextView txv = (TextView)currentFragment.findViewById(R.id.txv);
    txv.post( new Runnable() {
        @Override
        public void run() {
            getActivity()……
        }
    });
}

關鍵之一「畫面轉向」

Fragment 需要附加於 Activity,其生命週期也受 Activity 影響,這點在官方指南中的附圖可以明顯看出。實測畫面轉向時,Activity 確實執行 onDestroy() 與 onCreate(),而 Fragment 亦同。

Activity 與 Fragment 生命週期
(來源:https://developer.android.com/guide/components/fragments.html)


例在本問題中,轉向後會有兩個Fragment被建立:
  1. 隨Activity重建亦自動重建的Fragment → 原有重建的
  2. 當Activity再次執行onCreate所新建Fragment → 新建的
自動重建的 Fragment 一樣會依序呼叫 onAttach()、onCreate()、onCreateView() 與onActivityCreated(),且在這些方法內呼叫 getActivity() 仍會取得物件,因此不能在一開始的onAttach() 中判斷 getActivity() 是否為 null,作為是否繼續執行的依據。當然,在真正用到getActivity() 的當下,都判斷一次是否為 null 就可避免問題,但這很累人,讓我們繼續挖掘其原理。

關鍵之二「View.post()」

View.post 傳入的 Runnable 物件將被排入主執行緒 MessegeQueue 等待執行。然而隨著Activity 的 onCreate() 執行,一再新建並且 replace 的 Fragment 物件會使舊有 Fragment 銷毀,但已 post 去出的 runnable 仍在主執行緒中等待執行,當 runnable 被執行時,裡面的getActivity() 自然會取得 null,因為該 Fragment 已onDetach(參考前面的生命週期圖)。

解決方案

經過上述分析,大致有3方案可以使用:

  1. 直接在View.post() 判斷getActivity()是否為null,這是最簡單直覺的方法。
  2. 設法重用Fragment,在replace前檢查是已有存在的相同Fragment,這可利用Activity的onSaveInstanceState()來實踐。
  3. 利用Fragment的onAttach()方法,該方法自動傳入Activity參考,可在此先準備好要使用的資料與資源,但遷就其生命週期,程式的撰寫方式與執行流程可能需要隨之更動,若需暫存預先取出的資源,亦需考量耦合問題。

當然,直接鎖定Activity使其不能轉向亦可,但總有需要支援旋轉的情況。

參考資料

  1. Android官方文件Fragment
    https://developer.android.com/guide/components/fragments.html
  2. Android Fragment中getActivity()返回null的问题
    http://www.dss886.com/android/2015/08/11/01
  3. After the rotate, onCreate() Fragment is called before onCreate() FragmentActivity
    http://stackoverflow.com/questions/14093438/after-the-rotate-oncreate-fragment-is-called-before-oncreate-fragmentactivi
  4. 通过View.post()获取View的宽高引发的两个问题:1post的Runnable何时被执行,2为何View需要layout两次;以及发现Android的一个小bug
    http://blog.csdn.net/scnuxisan225/article/details/49815269
  5. 如何使用Handler
    http://givemepass-blog.logdown.com/posts/296606-how-to-use-a-handler
  6. Android中Thread、Handler、Looper、MessageQueue的原理分析
    http://blog.csdn.net/bboyfeiyu/article/details/38555547









2016年4月17日 星期日

Raspberry Pi 3 開機自動啟動程式

前言

最近在執行的專案需要在RPi開機時自動執行Python Script,在網路上爬了不少文章,但似乎沒有一篇可以完全解決問題的,多半是設定後沒有任何反應,經過一番拼湊,終於找出可以在RPi 3上正常執行的方法,在這邊記錄一下。

本文內容是基於這幾篇文章編寫,並視實測結果進行調整


原理

將RPi開機選項設定為以GUI啟動且自動登入User Pi,配合修改對應的autostart設定檔實現開機時自動執行某程式。

開機選項設定

設定步驟

  1. 開啟autostart設定檔,位於pi使用者下的 ~/.config/lxsession/LXDE-pi/autostart。視你目前操作情況,選用適當的編輯器開啟即可(例vi, nano, lefapad)。
  2. 在設定檔最下方填入以下指令。請將demo.py替換為你的python程式所在路徑。
  3. @/usr/bin/python demo.py
    
  4. 若設定無誤,重新啟動後應會看到 demo.py 已被執行。
  5. 若不想重啟,可直接下 startx 指令。



2016年2月14日 星期日

安裝Cordova Plugin

前篇「Cordova 安裝與開發流程」講述Cordova開發環境的準備及基本的使用教學,而本篇則是要說明如何在Cordova專案加入Plugin,讓你的專案具有使用相機、GPS、麥克風等諸多行動裝置硬體設備之能力。

以下就以裝載Camera的Plugin為例,在Cordova專案中裝載該插件:


取得與安裝Plugin

  1. 連線至Cordova官方提供的Plugin Search頁面 (https://cordova.apache.org/plugins/),輸入你想查找的關鍵字即可。我們在此輸入「camera」,馬上可以看到對應的Plugin已顯示。
    Codrova Plugins 搜尋頁面



    cordova-plugin-camera


  2. 使用指令 cordova plugin add cordova-plugin-camera,將會把camera插件下載並裝到Project/platforms目錄下的所有平台。


安裝原理簡述

執行plugin add 指令後,系統已在背後悄悄的完成許多異動,例如:


  1. 將Plugin檔案下載至Project/plugins目錄。
  2. 將已下載的「cordova-plugin-camera」安裝至各平台,這邊以Android為例,即為Project\platforms\android\assets\www\plugins 目錄。
  3. 依安裝的plugin 更新 Project\platforms\android\assets\www 目錄下的「cordova_plugins.js」。
  4. Project\platforms\android中的「AndroidManifest.xml」權限異動。
  5. android\src 目錄,新增「org.apache.cordova.camera」Package與所屬java檔。

雖然這些異動有跡可尋,但仍不建議自行修正,照著正規的方法來處理比較妥當。

檢視現有的 Plugin

使用指令 cordova plugin ls 即可。

移除 Plugin

使用指令 cordova plugin rm [plugin_name],例如要移除 Camera Plugin,則下 cordova plugin rm cordova-plugin-camera。若你忘了Plugin的全名,可由前面的 ls 命令找到。

延伸閱讀

Apache Cordova Documentation - Camera, https://cordova.apache.org/docs/en/3.3.0/cordova/camera/camera.html

2016年2月11日 星期四

Java Script 跨域請求

平日寫App慣了,在App呼叫遠端PHP程式存取資料庫是件相當自然的事。但近期有個Case是以Web方式開發,透過AJAX存取另一Server上的PHP時卻罷工了,追查一下發現是跨域請求造成的問題,在這邊筆記一下。


還原現場

  • 有一個存放於遠端Server的PHP程式,它會讀取資料庫並傳回結果,使用的資料格式為JSON。
query.php
<?php
 //存取資料庫,略!
 $arr = array(
  "id" => "a01",
  "name" => "elephant"
 );    
 echo json_encode($arr);
?>


  • 本機有一JavaScript,以jQuery提供的ajax function呼叫遠端PHP取回JSON字串。

search.js
$.ajax({
 method: "POST",
 url: "http://遠端Server IP/query.php",
 dataType: "json"
}).done(function(data) {
 alert(data.name);
}).fail(function() {
 alert("fail");
});


  • 執行看看,將跳出fail的alert。


錯誤訊息

目前主流的瀏覽器皆內建了開發人員工具,這邊以Firefox為例,執行前述提到的query.js,將看到如下的錯誤訊息:




相當親民的訊息,直接告訴你這是跨域請求的問題,缺少了Access-Control-Allow-Origin檔頭。



解決跨域資源請求之問題

在此以這篇文章「js跨域访问,No ‘Access-Control-Allow-Origin‘ header is present on」提供的方法來解。文內亦有提到跨域存取之細節,有興趣可以讀讀看。欲解決此問題,必須在前述的PHP程式加入「header("Access-Control-Allow-Origin: *");」。


<?php
 header("Access-Control-Allow-Origin: *");
 //存取資料庫,略!
 $arr = array(
  "id" => "a01",
  "name" => "elephant"
 );
 echo json_encode($arr);
?>




2016年1月31日 星期日

Android存取後端Server (part 1)

前言

若您開發的App有保存資料的需求,多半希望將資料存集至遠端伺服器,比較建議的作法是透過PHP, JSP, AST.NET等技術,以Web方式實作資料傳輸的媒介,存取資料即使用POST或GET的方式來進行。

在實作前有件事情需要注意,Android 4.0規定了存取網路的操作必須執行在另一Thread(執行緒)中,為了因應此規定,目前有兩大方向可供選擇:
  1. 使用Runnable與Handler:這種方法比較複雜,但彈性高,可依自己的需求規劃,同時在Handler的使用上需注意Memory leak之情況。
  2. 使用非同步任務(AsyncTask):此為相對簡單的方法,依規定好的格式即可以輕易完成網路存取。
接下來,就開始以第一種方式實作(Runnable + Handler),簡單一點的AsyncTask預計寫在下一篇(part2)。以下以重點說明為主,完整的範例已放在GitHub上,有需要的參考一下即可。


Step 0. 理解執行架構與流程

這邊用張簡圖表示,描述自步驟1發出請求起,至將遠端資料顯示至TextView的第驟6。Handler, Thread與Activity的互動關係大略是這樣。




Step 1. 開啟網際網路存取權限

為專案加入網際網路存取權限,找到AndroidManifest.xml,加入 android.permission.INTERNET 。


Step 2. HTTP請求與回應

以下程式摘錄自SendDataRunnable.java,可以直接套用,範圍程式中已把它包成method,若您想要自己另外處理,參數對照一下即可。這段程式碼有幾個重點:
  1. 第n列,有些教學範例會使用HttpClient這個類別來操作,但其已被Android棄用,因此這邊改以HttpURLConnection實作。
  2. 第n列,URL物件需要傳入Server上某個php檔案 (或其他Web程式)的路徑。
  3. 第n列,HTTP_OK這個常數值為200,表示正常狀態,確認正常了才開始讀取Response,你也可以再該處加上else,處理一些異常狀態。
  4. 第n列,composePostString( )負責組合參數,需傳入HashMap作為數個參數的集合(HashMap有Tag與Value對應的概念),詳見範例程式。
private String sendData(String target, HashMap params){
    String responseData = "";
    try {
        URL url = new URL(target);
        //建立與目標之間的連線
        HttpURLConnection connection = (HttpURLConnection)url.openConnection(); 
        connection.setRequestMethod("POST");
        connection.setDoInput(true);
        connection.setDoOutput(true);

        OutputStream stream = connection.getOutputStream();
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream, "UTF-8"));
        String c = composePostString(params);

        writer.write(c);

        writer.flush();
        writer.close();
        stream.close();
  
        int httpResponseCode = connection.getResponseCode();
        if(httpResponseCode == HttpsURLConnection.HTTP_OK) {
            String oneLine = "";
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(connection.getInputStream()));
            while ((oneLine = reader.readLine()) != null) {
                responseData += oneLine;
            }
        }

    } catch (MalformedURLException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }

    return responseData;
}



Step 3. 實作Run()

run( )是Runnable介面中唯一的方法,當其被執行時會自動呼叫,在範例程式中拿來呼叫Step 2.所寫的sendData( )。

此段程式相當簡單,sendData( )會傳回Server回應的資料,故需檢查資料長度是否正常,並針對各情況進行處理。傳回來的資料會放在response變數中,至此所有動作仍不在Android的主執行緒上執行,若你要在這邊把資料更新到UI上是無法存取的,所以這邊有個handler.obtainMessage( ),用它把資料傳至主執行緒上的Handler。

但有一點請注意,obtainMessage傳入兩個參數,第一個是int型別的值,這是讓你識別目前的情況用的,姑且稱它為「識別碼」,你可以自己決定該如何實作,例如:1代表新增成功、2代表刪除成功、3代表取得清單資料等。為了方便管理,也可以宣告常數來處理,讓程式更易維護。


@Override
public void run() {
    String response = sendData(this.path, this.params);
    if(response.length() > 0) {
        //TODO
        handler.obtainMessage(0, response).sendToTarget();
    }else{
        //TODO
    }
}



Step 4. Handler

在上一步驟中,我們已經取得Server回應的字串資料,為了在主執行者中接收這個字串資料,需要準備Handler來承接,礙於Memory Leak的問題(有機會再談),建議以內部靜態類別來實作,完整程式請參考範例中的main.java。

這邊定義了HttpHandler類別,其繼承Handler (來自android.os.Handler,別import錯了)。

請特別注意幾個地方:
  1. 第n列,將WeakReference強迫轉型為Main,這個Main需繼承當初建立WeakReference傳入的泛型 (本例是Activity),白話的說Main是Activity,但其具有Activity沒有的成員與方法(因為繼承),因此我們要把它強迫轉型為Main,以方便進行後續處理。
  2. 第n列,這邊用if判斷了msg.what值,該值就是在Step 3所傳入的int參數(本例為0),所以這邊檢查等於0時把Server傳回的response字串顯示在TextView上。
  3. 承上,response字串也是是在Step 3傳入,但其型別為Object,所以這邊取出時要再強迫轉型為字串。


static class HttpHandler extends Handler {
    WeakReference weakReference;
 
 public HttpHandler(Activity activity) {
        weakReference = new WeakReference<>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
        Main main = (Main)weakReference.get();
        if(msg.what == 0) {
            //TODO 依what判斷後續處理
            main.txvResponse.setText((String)msg.obj);
        }
    }
}



Step 5. 串接一切

各細節寫好了,現在要把他們串起來!實際上該封裝的東西也都裝好了,所以真正在呼叫時動作不會太多。

首先,在呼叫前確保handler與params兩個物件有新建了,兩者的作用在前面都有提過。
handler = new HttpHandler(this);
HashMap<String, String> params = new HashMap<>();
接著填入要送至Server的參數,每個參數都會有Tag (或稱為Name)與Value的對應。
params.put("name", "tom");

最後要將整個HTTP的請求與回應動作放在另一個執行緒中執行,因此我們這麼處理。記得網址的部分自己換掉。
new Thread(
    new SendDataRunnable(handler, "http://www.example.com", params)
).start();


小結

上述的程式還可以再封裝的更細緻,但有時封過頭會綁手綁腳的,大家若要直接使用範例程式,請注意裡面標示//TODO的地方。
這邊有小建議,請盡可能的將識別碼、網址或ip之類的資源統一管理,看是要宣告成常數,還是把它放在string.xml中。
還有!安全議題也需注意,本篇沒有特針對安全部分進行討論,若你要傳輸的資料是有安全顧慮的,請以https的方式進行,亦需留意Server端的安全措施。


2016年1月10日 星期日

IoT App 開發工具 Evothings 介紹


以下內文已於 2016/4 針對正式釋出的 2.0 版進行微調。

Evothings是一套針對物聯網(IoT)相關行動App開發工具,開發技術採用Cordova。它內建了不少相關範例程式(BLE/Beacon、Arduino、LinkIt One,甚至是ESP8266等相關應用)。

Evothings主要分為2大部分:執行在PC上的Evothings Workbench與執行在行動裝置上的Evothings Viewer (小於2.0版的是使用Evothings Client)。以下內文內容以Evothings 2.0 為基準。

Evothings提供的Viewer程式已事先裝載了相關Plugin,透過Workbench可將你的開發環境(PC)與Viewer(Mobile)串接,優點是每次變更程式時,不需手動編譯與打包,更動的結果會即時反應在行動端。目前Evothings不能完成打包的動作,主要用途是用於快速開發原型(prototype)與實驗目前的想法是否可行。

接著開始看看如何使用Evothings:


下載Evothings Workbench

至官方網站下載即可,本文以Evothings Workbench 2.0 為介紹對象。
https://evothings.com/


執行Workbench

以Windows平台而言,下載後解壓縮即可,執行其中的「EvothingsWorkbench.exe」即可啟動,需無進行安裝動作。

安裝Evothings Viewer

記得這邊談的是2.0以上版本,請在行動裝置安裝Evothings Viewer。若是使用較早的Workbench搭配的是Evothings Client。在Apple App Store或Google Play搜尋Evothings即可找到相應App,將其安裝即可。




Workbench與Viewer連線

2.0版本可以透過外網連線,也因此會有KEY的產生與登入之功能,在2.0以前是Workbench與Viewer都是區網的情況下才可使用
切換至Workbench的Connect頁籤,按下「GET KEY」即可產生金鑰,並在Viewer中輸入金鑰即可連線。 (實際使用時有遇過timeout的情況,再多試幾次即可)。




範例程式的使用

前面有提到,Evothings內建了不少IoT相關應用範例,這些範例都可以在Workbench的Examples頁籤中找到。2.0版與先前版本的差異是,在使用這些範例要先按下「COPY」把它複製到你的開發目錄(依你的習慣設定即可),完成複製的專案可以在My Apps頁籤中找到。早期版本是直接修正範例檔,若沒有事先備份可能會回不去。


修改範例程式並執行

在Workbench中切換至My Apps頁籤,在想要修改的專案中按下「CODE」即會開啟放置原始碼的目錄,使用你習慣的程式碼編輯器開啟相關檔案即可。這邊用Notepad++打開index.html。在此時你可以修正這些檔案的內容,包含html、js與css等,一經修正並存檔,即會自動把結果反應至行動裝置。若你發現修正後行動裝置並沒有跟著改變,可能是Workbench與Viewer之間的連線中斷了,請檢查看看。






打包釋出

如同前述,目前Evothings沒有提供這個功能,但由於專案骨子裡是Cordova,因此可以用Cordova自己的打包流程來產出最終結果。這部分可以先參考本Blog的文章「Cordova 安裝與開發流程」會較有概念。



其他功能

若你有原本的Cordova專案,可以直接把專案中的主html檔拖拉至My App頁籤,即可透過Evothings幫你快速執行至實機。



而在Viewer右上角的選單可以打開「Info」頁面,即可查詢目前包含的Plugin,可以發現有battery、camera、geolocation等常用Plugin,以及特定用途的像是ble與beacon。


2015年10月26日 星期一

解決 Android Studio 設定檔占用C磁碟之問題

Android Studio 的設定檔預設存放於 C 磁碟,包含 config 與 system 兩個資料夾,使用至今大約有230 MB,若你的 C 碟空間很保貴,可以考慮更改預設的儲存路徑。

這種問題多半在行時 Android Studio 會出現警告「Low disk space on a android studio system directory partition.」,若磁碟真的滿了,後續動作將無法正常執行。

依這篇文章「Android Studio使用问题小结」裡面提供了解決方法,但目前(2015/10)的版本似乎有些出入,但大致上是沒有問題的。

路徑設定步驟

  1. 找到 Android Studio 的安裝路徑,展開至 AndroidStudio\bin。
  2. 用文字編輯器開啟 idea.properties。
  3. 直接新增設定,包含 idea.config.path=D:/xxx/config 與 idea.system.path=D:/xxx/system。

請注意,原本的設定檔若放在 C 碟,可以直接把它搬到上面第3點指定的路徑,讓其能夠正常對應即可。