一、NhmFramework Android端的消息處理機制原理
1、概要表述:在我們的框架中,Android客戶端通過繼承Application來控制整個應用程序的生命周期,在Application onCreate()方法中,我們將啟動一個MainService,這個Service將負責Activity的異步消息處理(包括異步Http請求)、任務調度、數據共享等大部分持久化操作。那么這樣做的目的何在呢?
1)異步消息處理:在Service中實現異步消息處理是為了將Activity的界面顯示的操作保持在一個單獨的線程中,而將其他占用時間的操作(如訪問服務器、解析數據、處理數據、GPS定位等)放在一個新的線程中,這樣UI界面將不會“等待”這些耗時過多的任務,也就是說在程序處理這些任務的時候,UI界面依然可以操作,不會卡死,實現更好的用戶體驗。當然,你也可以將耗時線程直接在Activity中新建,但這樣的處理方式就無法管理眾多的Task,不利於程序結構和代碼復用。
2)任務調度:我們采用了一個類似棧的數據結構來管理Task,具體流程是這樣的:
UI界面新建一個訪問服務器的Task—>把這個Task壓到Service的Task棧中—>MainService執行任務—>將結果更新到UI—>UI把結果顯示出來
3)數據共享:MainService為Activity間傳遞參數提供了橋梁,尤其是一些可能被不同的Activity多次使用的數據,如GPS位置等,通過MainService保存變量比在Activity間直接傳參更加方便。
2、要點理解:
1)我們知道,Android的Service本身並不是一個單獨的Thread——也就是說,在這個Service啟動的時候,並沒有新的線程建立,於是我們必須自己來建立一個新的線程監聽從UI發過來的任務請求,經過后台處理后再將結果發給UI,實現UI異步處理的需求。
2)繼承Application也並不會建立新的線程,也就是說繼承Application不會耗費更多的系統資源,即使你不繼承、Application的生命周期也存在,你繼承了,只是可以在生命周期中為程序在不同時序階段“添加”更多的處理。
3)MainService中的消息近處理進程是通過一個While(true)循環來監視Task棧,任務棧里有任務了,就執行,執行結果通過消息的方式發到UI層,做過C/S的讀者肯定都會了解這樣的機制。
3、GetEmployeeByID為例的時序圖:
二、Android程序的啟動與完全退出
1、啟動:Android程序的啟動入口,是在manifest.xml中聲明 <action android:name="android.intent.action.MAIN" />,而在這之前,如果你已經繼承了Application,並且在manifest.xml中聲明這個繼承,系統將會執行你的Application類。在我們的項目中,MainService就是在Application中啟動的。
2、退出:還記得2011年年中的時候,一些Android應用被爆出無法完全退出的風波,拋開蓄意的成分,單從技術層面來講,Android的退出確實不是一條命令那么簡單。首先,如果你的程序中有多個Activity,則必須所有的Activity都要執行 Finish();其次如果你的程序新建了Service,則這個Service需要執行intent.stopService()方法;而Process.killProcess()在不同的Android版本又有不同的解讀……所以我們最應該先弄懂的是,怎么判斷一個Android程序是否完全退出了。
1)打開Eclipse,Debug你的程序,在Debug選項卡下會看到下圖這樣的狀態:
2)切換到DDMS,在Devices中,你會看到這樣的狀態:
可以看到,最下面那個進程(DDMS中查看的是進程process,Debug中查看的是線程thread)就是我們正在調試的程序。此時我退出程序,如果這個進程不見了,則證明完全退出。也就是要在DDMS中看是否這個進程被kill了,才能判斷是真正的完全退出。
那正確的退出方法是什么樣的呢?Android程序的正確退出要遵循如下順序:
1)遍歷所有Activity執行Finish()方法;
2)遍歷你程序中建立的Service,並且stopService();
3)killprocess
除此之外,你還要特別注意執行以上操作的位置:Service不能自己停止自己,所以如果你要stopService()必須在Application里,killproces也是同理。如果你在Service里執行了stopService()等操作,也不會有錯誤提示,但在DDMS中,你會發現這個process依然存在,雖然偶此時Debug窗口中你的程序已經Disconnect,而且在模擬器中程序也已經“不見了”——只要你遍歷所有Activity並執行finish()方法,那么模擬器中正在運行的程序就會消失,看起來好像“退出”程序一樣,而在后台,可能依然有你建立的Service在運行。
這里給出我的AndroidMenifest.xml和MainApp.Java
AndroidMenifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="nhm.atraining" android:versionCode="1" android:versionName="1.0">
<uses-sdk android:minSdkVersion="7" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application android:debuggable="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:name="com.nhm.training.logic.MainApp">
<service android:name="com.nhm.training.logic.MainService">
<intent-filter>
<action android:name="com.nhm.training.logic.MainService"></action>
</intent-filter>
</service>
<activity android:label="@string/app_name" android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
MainApp.Java
public class MainApp extends Application {
public MainApp mApp;
@Override
public void onCreate() {
mApp = this;
Intent serv = new Intent("com.nhm.training.logic.MainService");
this.startService(serv);
super.onCreate();
}
@Override
public void onTerminate() {
// TODO Auto-generated method stub
super.onTerminate();
}
public void destroy() {
// TODO Auto-generated method stub
for (Activity ac : MainService.allActivity) {
ac.finish();
}
Intent it = new Intent("com.nhm.training.logic.MainService");
this.stopService(it);
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
}
三、利用MainService實現異步Task機制
通過上面的原理,相信讀者已經理解了MainService的主要作用,下面我們就來實現MainService。在com.nhm.training.logic包中,包含了任務處理所需要的各個類,下面逐個解釋下它們的作用。
IActivity: 是一個接口聲明,聲明了void init()和void refresh(Object ...param)兩個接口,其作用主要是要求各個Activity必須實現初始化和更新方法才能實現Task機制。
MainApp:繼承於Application,主要負責控制應用程序在啟動和退出的時候開啟和停止MainService,在后期使用第三方API時(如百度地圖)可能也會在Application層做某些處理。
MainService:完成整個Task消息處理機制,在MainService onCreate()時拋出新線程監聽Task,當有Task進入時執行任務、任務結束后發消息到UI。
ServiceConst:保存一些必要的靜態常量。
Task:描述任務的類,包括任務ID、任務參數等。
本想為MainService先畫一個UML Class Diagraim再給出代碼,結果機器的Rose出了點問題,反向JAVA工程總是異常退出,唉,所以就直接給出代碼吧,還望各位讀者海涵。代碼比較長,沒有類圖還真不太好看啊。
public class MainService extends Service implements Runnable {
public static ArrayList<Activity> allActivity = new ArrayList<Activity>();
public static int lastActivityId;
public static String lastActivityName;
private UEHandler ueHandler;
public static List<Task> tasklist = new ArrayList<Task>(0);;;
public static ArrayList<Task> allTask = new ArrayList<Task>();
// 從集合中通過name獲取Activity對象
public static Activity getActivityByName(String name) {
for (int i = allActivity.size() - 1; i >= 0; i--) {
Activity ac = allActivity.get(i);
if (ac.getClass().getName().indexOf(name) >= 0) {
return ac;
}
}
return null;
}
// 添加
public static void newTask(Task task) {
allTask.add(task);
}
public boolean isrun = true;
@Override
public IBinder onBind(Intent intent) {
// TODO Auto-generated method stub
return null;
}
// 更新UI
private Handler hand = new Handler() {
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
Bundle bTCCGF = msg.getData();
String acname = bTCCGF.getString("acName");
IActivity ia = (IActivity) MainService.getActivityByName(acname);
switch (msg.what) {
case ServiceConst.GET_EMPLOYEE_ERROR:
ia.refresh(ServiceConst.GET_EMPLOYEE_ERROR, msg.obj);
break;
case ServiceConst.TASK_GET_EMPLOYEE:
ia.refresh(ServiceConst.GET_EMPLOYEE_OK, msg.obj);
break;
}
}
};
public void setCurrentActivityName() {
ActivityManager activityManager = (ActivityManager) getApplicationContext()
.getSystemService(Context.ACTIVITY_SERVICE);
List<RunningTaskInfo> forGroundActivity = activityManager
.getRunningTasks(1);
RunningTaskInfo currentActivity;
currentActivity = forGroundActivity.get(0);
String activityName = currentActivity.topActivity.getClassName();
lastActivityName = activityName;
}
// 執行任務
public void doTask(Task task) {
setCurrentActivityName();
Message mess = hand.obtainMessage();
mess.what = task.getTaskID();// 定義刷新UI的變化
tasklist.add(task);
HashMap paramIn = (HashMap) task.getTaskParam();
HashMap paramOut = new HashMap();
String acname = String.valueOf(paramIn.get("acname"));
paramOut.put("acName", acname);
Employees emp = null;
switch (task.getTaskID()) {
case ServiceConst.TASK_GET_EMPLOYEE:
try {
int uid = (Integer.valueOf(String.valueOf(paramIn
.get("EmployeeID"))));
emp = Bll.GetEmployeeByID(uid);
mess.obj = emp;
} catch (Exception e) {
mess.what = ServiceConst.GET_EMPLOYEE_ERROR;
}
if (emp == null) {
mess.what = ServiceConst.GET_EMPLOYEE_ERROR;
}
break;
}
Bundle bc = new Bundle();
bc.putString("acName", acname);
mess.setData(bc);
hand.sendMessage(mess);// 發送更新UI的消息給主線程
allTask.remove(task);// 執行完任務
}
@Override
public void run() {
// TODO Auto-generated method stub
while (isrun) {
Task lastTask = null;
synchronized (allTask) {
if (allTask.size() > 0) {// 取任務
lastTask = allTask.get(0);
// 執行任務
doTask(lastTask);
}
}
// Log.d("debug main Service", ".............");
try {
Thread.sleep(1000);
} catch (Exception e) {
}
}
}
@Override
public void onCreate() {
// TODO Auto-generated method stub
// Log.d("debug main Service Oncreate", ".............");
super.onCreate();
ueHandler = new UEHandler(this);
// 設置異常處理實例
Thread.setDefaultUncaughtExceptionHandler(ueHandler);
isrun = true;
Thread t = new Thread(this);
t.start();
}
@Override
public void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
isrun = false;
}
// alertUser 提示用戶網絡狀態錯誤
public static void AlertNetError(final Context con) {
AlertDialog.Builder ab = new AlertDialog.Builder(con);
ab.setTitle(R.string.NoRouteToHostException);
ab.setMessage(R.string.NoSignalException);
ab.setNegativeButton(R.string.apn_is_wrong1_exit,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// TODO Auto-generated method stub
dialog.cancel();
// exitApp(con);
((MainApp) ((Activity) con).getApplication())
.destroy();
}
});
ab.setPositiveButton(R.string.apn_is_wrong1_setnet,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// TODO Auto-generated method stub
dialog.dismiss();
con.startActivity(new Intent(
android.provider.Settings.ACTION_WIRELESS_SETTINGS));
}
});
ab.create().show();
}
public static void promptExit(final Context con) {
// 創建對話框
LayoutInflater li = LayoutInflater.from(con);
View exitV = li.inflate(R.layout.exitdialog, null);
AlertDialog.Builder ab = new AlertDialog.Builder(con);
ab.setView(exitV);// 設定對話框顯示的View對象
ab.setPositiveButton(R.string.exit, new OnClickListener() {
public void onClick(DialogInterface arg0, int arg1) {
// TODO Auto-generated method stub
((MainApp) ((Activity) con).getApplication()).destroy();
}
});
ab.setNegativeButton(R.string.cancel, null);
// 顯示對話框
ab.show();
}
public static void init() {
// TODO Auto-generated method stub
}
}
四、客戶端擴展——命令拼接與加密
上一篇我們已經提到了,要用一個HashMap來保存從客戶的到服務器的參數,這樣做的目的是為了更加清晰的在BLL層中描述發送到URL的參數。代碼如下:
/**
* 將URL參數哈希表轉換為加密字符串
* @param map
* @return
*/
public static String toURLParam(HashMap map) {
String cmd = String.valueOf(map.get("cmd"));
//明確空cmd為"unknown"
if (cmd.equals(null) || cmd.equals("")) {
cmd = "unknown";
}
//添加公共參數字段,如imei,gps位置等,這里例子僅添加一個當前時間
map.put("now", (new Date()).toString());
//HASHMAP先轉換為JSON
JSONObject jstest = new JSONObject(map);
String jsparam = jstest.toString();
//加密壓縮JSON,加入你自己的加密算法
jsparam = DESEncoder.encrypt(jsparam);
String enc = "";
//最外層用Base64Encode包裝一下,注意將Base64的字符表中的 + 和 / 替換
try {
enc = new BASE64Encoder().encode(jsparam.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
enc="";
e.printStackTrace();
}
return enc;
}
舉一個例子說明在BLL層中如何實現HTTP請求
public static Employees GetEmployeeByID(int id)
{
HashMap param = new HashMap();
param.put("cmd", "GetEmployee");
param.put("method", "ByID");
param.put("id", id);
//將此HashMap轉換為加密字符串
String parmstr = URLParamUtils.toURLParam(param);
String paramstrall = baseURL+ "api/android_process.aspx?a=" + parmstr;
Employees emp = new Employees();
try {
//使用Http.get()連接 返回JsonArray
JSONArray json = get(paramstrall,null, true).asJSONArray();
String jsonfirst = json.get(0).toString();
Log.d("BLL","Start gson.fromJson");
//新建Gson對象並設置與服務器發來相同格式的Date類型
Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create();
//反序列化Json數據為 Employees類型
emp = gson.fromJson(jsonfirst, Employees.class);
Log.d("BLL","End gson.fromJson");
} catch (NException e) {
// TODO Auto-generated catch block
emp=null;
e.printStackTrace();
} catch (JSONException e) {
// TODO Auto-generated catch block
emp=null;
e.printStackTrace();
}
return emp;
}
五、客戶端UI——MainActivity
有了上面的基礎,MainAcitivity就很簡單了。在Activity onCreate()的時候,組建任務並將任務壓到任務棧,任務在MainService新建的線程中執行,而前台可以用一個Progress顯示“正在讀取”的進度條,當Task執行完成后,回發到Activity更新,Activity根據返回的參數情況顯示出來。下面給出MainActivity代碼:
public class MainActivity extends Activity implements IActivity {
/** Called when the activity is first created. */
protected static View process;// 加載條
//后退鍵顯示退出提示
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// TODO Auto-generated method stub
if (keyCode == KeyEvent.KEYCODE_BACK) {
MainService.promptExit(this);
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
process = this.findViewById(R.id.progress);
//默認顯示進度條
process.setVisibility(View.VISIBLE);
//將Activity保存在MainService中
MainService.allActivity.add(this);
//建立參數HashMAP
HashMap param = new HashMap();
param.put("EmployeeID", new Integer(2));
param.put("acname", "MainActivity");
//建立Task
Task task = new Task(ServiceConst.TASK_GET_EMPLOYEE, param);
//壓入MainService的任務棧
MainService.newTask(task);
}
@Override
public void init() {
// TODO Auto-generated method stub
}
@Override
public void refresh(Object... param) {
// TODO Auto-generated method stub
//處理UI更新
TextView tvEmpName = (TextView) this.findViewById(R.id.empName);
// 隱藏進度條
process.setVisibility(View.GONE);
switch (((Integer) (param[0])).intValue()) {
case ServiceConst.GET_EMPLOYEE_OK:
//服務器返回正確的情況
Employees emp = (Employees) param[1];
tvEmpName.setText(emp.getFirstName());
break;
case ServiceConst.GET_EMPLOYEE_ERROR:
//服務器返回錯誤的情況
tvEmpName.setText("error occered");
break;
}
}
}
五、服務端擴展——客戶端命令解析處理
在客戶端向服務器端發出HTTP請求時,實際上是請求了服務器端的網址:http://localhost:8080/api/android_process.aspx?a=加密字符串 ,我們需要在服務端解析Request.Querystring["a"]、解密字符串、還原成JSONObject,並在服務端的BLL層做相應處理。下面給出服務端android_process.aspx和EmployeeBLL.aspx的C#代碼:
public partial class android_process : System.Web.UI.Page
{
string instr;
JSONObject paramJSON;
protected void Page_Load(object sender, EventArgs e)
{
string rst="";
try
{
//獲得加密串
instr = Request.QueryString["a"];
//解析加密串為JSONObject
string orgStr = EncryptHelper.DecodeBase64WithDES(instr);
paramJSON = JSONConvert.DeserializeObject(orgStr);
//取cmd命令,如果取得空值或獲取失敗,則返回錯誤
string cmd = "";
cmd = paramJSON["cmd"].ToString();
if (String.IsNullOrEmpty(cmd))
{
lt_rtn.Text = "{rst:error}";
return;
}
//根據cmd命令跳轉邏輯
switch(cmd)
{
case "GetEmployee":
rst = EmployeeBLL.getEmployeeInfo(paramJSON);
break;
}
}
catch (Exception)
{
lt_rtn.Text = "{rst:error}";
}
lt_rtn.Text = rst;
}
}
public static class EmployeeBLL
{
public static string getEmployeeInfo(JSONObject paramJSON)
{
string method = paramJSON["method"].ToString();
string rtn = "";
switch (method)
{
case "ByID":
int id = int.Parse(paramJSON["id"].ToString());
EmployeeEntity ee = new EmployeeEntity(id);
rtn = ee.toJson();
break;
}
return rtn;
}
}
六、模擬器調試
下面我們再模擬器里Debug一下:
一)正常測試:
二)異常測試
在正常測試中,我們要獲得ID=2的員工的姓名,這里我們將ID改為-1,由於這個員工不存在,服務端將返回異常,我們看看客戶端的處理。
可以看到客戶端正確處理了異常。
總結
這次的課程我們通過一個駐留內存的Service實現了Android的異步任務機制,並將這種異步處理應用到Http訪問中,讀者可以繼續擴展,利用這樣的機制來處理GPS、上傳等耗時操作。
從下一篇開始,我們將分成兩個分支,一個分支繼續學習Android與.net服務器交互的知識,另一個分支,將轉向IOS平台,讓IOS也支持我們的.net服務端。









