從零開始搭建架構實施Android項目

咱們先假設一個場景需求:剛有孩子的爸爸媽媽對用照片、視頻記錄寶寶成長有強烈的意願,但苦於目前沒有一款專門的手機APP作這件事。A公司洞察到市場需求,要求開發團隊儘快完成 Android客戶端的開發。如下模擬團隊和工做開展。
  • 團隊狀況:產品經理1人,Android開發2人,服務端開發2人,UI設計1人。
  • 開發週期:兩個月。
  • 工做量:大約50個界面。
  • 隱含需求:考慮到用戶羣體有可能激增的狀況,服務端須要有必定的併發能力。
  • 前提:原型已設計完成。

1 服務端概要設計

1.1 系統架構

  先給出服務端的架構圖。html

  因爲服務端開發有Java、PHP背景,爲了快速完成開發任務,咱們選擇PHP做爲服務端開發語言,順便也把數據庫定爲MySQL。考慮後期擴展和數據庫訪問性能,擬引入Redis非關係型數據庫。同時爲了提升數據讀的性能,在雲服務器和數據庫之間用上緩存,併爲數據庫主從備份、讀寫分離。服務器就不搭建在本地了,管理是一大問題。如今雲服務器一大把,七牛、阿里雲、騰訊雲、百度雲、金山雲等等,技術成熟,並且價格還算公道。在此咱們選擇阿里雲。爲了應對可能面臨的併發問題,雲服務器要考慮負載均衡。項目中可能存在大量的須要上傳和下載照片和視頻,咱們選擇阿里雲的開放存儲服務,同時爲了提高各個地區的下載體驗,咱們引入CDN。客戶端經過API Service和服務端交換數據,圖片和視頻的下載直接經過CDN。android

1.2 模塊劃分

  根據需求和原型設計,可能的模塊劃分以下:git

  • 註冊登陸模塊
  • 用戶模塊
  • 小孩模塊
  • 媒體(圖片+視頻)模塊
  • 相冊模塊
  • ……

1.3 數據交換和API接口

  服務端與客戶端使用JSON交換數據,使用自定義JSON格式,約定返回code、message,實體封裝在result中,支持單個實體、實體列表、多個實體列表,定義以下:github

{
    "code":500,
    "message":"系統異常,請稍後重試",
    "result":""
}
複製代碼
{
    "code":200,
    "message":"登陸成功",
    "result":{
        "user":{
            "userId":1,
            "nickName":"Leo",
            "email":"Leo@xxx.com",
            "gender":0
        }
    }
}
複製代碼
複製代碼
{
    "code":200,
    "message":"SUCCESS",
    "result":{
        "album":{
           "kid":{
                 "kidId":1,
                 "nickName":"LEE",
                 "gender":1,
                 "birthday":"2014-3-6",
......    },   "media":{ "mediaId":123, "mediaType":1, "createdTime":193743546746, "mediaDescription":"這是小孩第一次出去春遊",
......    }    }  }
}
複製代碼

   主要API接口設計以下:數據庫

複製代碼
http://api.xxx.com/service/v1.0/user/login
http://api.xxx.com/service/v1.0/user/third-login
http://api.xxx.com/service/v1.0/user/register
http://api.xxx.com/service/v1.0/user/logout
http://api.xxx.com/service/v1.0/user/info/update
http://api.xxx.com/service/v1.0/album/upload
http://api.xxx.com/service/v1.0/album/update
http://api.xxx.com/service/v1.0/album/delete
......
複製代碼

  也許你看到了,API作了二級域名映射,同時爲了服務端後期API版本的升級管理,在URL中加上了版本標識V1.0。命名方面我儘可能作到restful的風格。對了,此處沒有使用Https。爲了解決數據傳輸的安全,我作了點特別的處理:對請求體和響應結果進行RSA加密(若是服務端返回的數據稍稍過大,這個RSA嚴重影響客戶端解密,後來我換成了AES),全部請求爲POST請求,因此API URL後面沒有帶參數,你也看不到任何請求相關的信息。api

1.4 數據庫設計

  根據需求和原型設計,數據庫的設計大概須要兩週時間。其實一週基本搞定了,但爲了考慮充分,留出一週時間來檢驗和調整。數據庫E-R圖略。緩存

2 Android客戶端

2.1 基本結構

  Android自己就是MVC,因此我不打算引入MVPMVVM。個人理念是職責分層,快速推出Android 1.0。主要的包結構以下:安全

工程的搭建和包的劃分有各類各樣的,適合本身的就好了。想討論或想看別人怎麼作的,點擊這裏:App工程結構搭建:幾種常見Android代碼架構分析服務器

2.2 功能劃分

  註冊登陸,我的信息,個人小孩,相冊管理,消息通知,系統設置等等。restful

2.3 引入的第三方技術

  重複發明輪子是不可取的。有些模塊根本不必本身寫。如下是引入的第三方庫,以及優點說明。

2.3.1 網絡請求庫android-async-http

  • 在匿名回調中處理請求結果
  • 在UI線程外進行http請求
  • 文件斷點上傳
  • 智能重試
  • 默認gzip壓縮
  • 支持解析成Json格式
  • 可將Cookies持久化到SharedPreferences

2.3.2 雲巴推送

  • 專一於爲須要實時數據交換的產品提供完美解決方案
  • 基於發佈者/訂閱者(publisher/subscriber)模式,集成簡單
  • 對比了百度雲推送、騰訊信鴿推送,雲巴效果更好
  • 原極光推送CTO創辦的

2.3.3 xUtils(只使用其中的DbUtils和ViewUtils)

  • Android中的ORM框架,一行代碼就能夠進行增刪改查
  • 支持事務,默認關閉
  • 可經過註解自定義表名、列名、外鍵、惟一性約束、NOT NULL約束、CHECK約束等(須要混淆的時候請註解表名和列名)
  • Android中的IOC框架,徹底註解方式就能夠進行UI,資源和事件綁定
  • 新的事件綁定方式,使用混淆工具混淆後仍可正常工做

2.3.4 友盟統計

  • 國內專業的移動應用統計分析平臺
  • 統計和分析流量來源、內容使用、用戶屬性和行爲數據
  • Crash log跟蹤

2.3.5 雲通信驗證碼

  • 解決方案成熟,衆多公司使用

2.3.6 高德地圖定位

  • GPS+基站+wifi的混合定位方式
  • 接入簡單

2.3.7 異步圖片加載庫Android-Universal-Image-Loader

  • 多線程下載圖片,圖片能夠來源於網絡,文件系統,項目文件夾assets中以及drawable中等
  • 支持隨意的配置ImageLoader,例如線程池,圖片下載器,內存緩存策略,硬盤緩存策略,圖片顯示選項以及其餘的一些配置
  • 支持圖片的內存緩存,文件系統緩存或者SD卡緩存
  • 支持圖片下載過程的監聽
  • 根據控件(ImageView)的大小對Bitmap進行裁剪,減小Bitmap佔用過多的內存
  • 較好的控制圖片的加載過程,例如暫停圖片加載,從新開始加載圖片,通常使用在ListView,GridView中,滑動過程當中暫停加載圖片,中止滑動的時候去加載圖片
  • 提供在較慢的網絡下對圖片進行加載

2.3.8 阿里雲OSS Android客戶端SDK

  • 提供文件(圖片、視頻等等)上傳
  • 大文件分塊上傳
  • 刪除操做(不推薦在客戶端使用)

2.3.9 組件內通信EventBus

  • 基於發佈者/訂閱者(publisher/subscriber)模式
  • 簡化了應用程序內各組件間、組件與後臺線程間的通訊

2.3.10 Android本地數據庫加密庫SQLCipher

  • 基於SQLite擴展的開源數據庫,在SQLite的基礎之上增長了數據加密功能
  • SQLCipher對Android SDK中全部與數據庫相關的API都製做了一份鏡像,使得開發者能夠像操做廣泛的數據庫文件同樣來操做SQLCipher

2.4 基礎組件封裝

2.4.1 基礎回調接口

複製代碼
public interface DataCallback {
    
    void onSuccess(Object result);
    
    void onFailure(Object result);
    
}
複製代碼

2.4.2 網絡訪問

  先看一下登陸的序列圖:

  HttpManager類負責調用AsyncHttpWrapper中的post方法,和對服務端返回的數據解密、JSON轉對象、回調上層;AsyncHttpWrapper則負責請求體的封裝加密和其它的校驗參數封裝。看一下HttpManager類的post方法:

複製代碼
public void post(Context context, String url, RequestParams params, final String modelName,
                     final DataCallback callback) {
        AsyncHttpWrapper.post(context, url, params, new AsyncHttpResponseHandler() {
            @Override
            public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) {
                try {
                    if (modelName != null) {
                        handleResponse(responseBody, callback, modelName);
                    } else {
                        String response = new String(responseBody);
                        // 解密
                        response = AES128.getInstance().decrypt(AppUtil.decodeReplace(response));
                        
                        // JSON轉對象
                        BaseMessage message = AppUtil.getMessage(response);
                        if (callback != null) {
                            // 若是自定義code是200
                            if (Coder.CODE_200.equals(message.getCode())) {
                                callback.onSuccess(message.getMessage());
                            } else {
                                callback.onFailure(new ServerError(message.getCode()));
                            }
                        }
                    }
                } catch (JSONException e) {
                    LogUtil.e(e);
                    callback.onFailure("服務端返回的數據不能解析成JSON");
                } catch (Exception e) {
                    LogUtil.e(e);
                    callback.onFailure(e);
                }
            }
            
            @Override
            public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) {
                if (callback != null) {
                    callback.onFailure(error);
                    if (responseBody != null) {
                        String s = new String(responseBody);
                        LogUtil.e(s);
                    }
                }
            }
        });
    }
複製代碼

  AsyncHttpWrapper中的post方法

複製代碼
public static void post(Context context, String url, RequestParams params, AsyncHttpResponseHandler responseHandler) {
        // 設置請求頭部信息
        generateHeader(context);
        
        // 加密請求參數
        String encryParams = AES128.getInstance().encrypt(params.toString());
        
        RequestParams requestParams = new RequestParams();
        requestParams.put("param", AppUtil.encodeReplace(encryParams));
        
        client.post(context, url, requestParams, responseHandler);
    }
複製代碼
private static AsyncHttpClient client = new AsyncHttpClient();

2.4.3 Adapter封裝

  爲了加快開發速度,重用代碼,Adapter的使用有技巧。每次在getView中查找控件id、利用ViewHolder、賦值,最後返回convertView,看着都是差很少的代碼。是時候脫離這個苦海了。先看怎麼解決共用的ViewHolder問題。

複製代碼
public static <T extends View> T get(View view, int id) {
        SparseArrayCompat<View> viewHolder = (SparseArrayCompat<View>) view.getTag();
        if (viewHolder == null) {
            viewHolder = new SparseArrayCompat<>();
            view.setTag(viewHolder);
        }
        View childView = viewHolder.get(id);
        if (childView == null) {
            childView = view.findViewById(id);
            viewHolder.put(id, childView);
        }
        return (T) childView;
    }
複製代碼

  ViewHolder的做用,就是經過convertView.setTag與convertView進行綁定。當convertView複用時,直接從與之對應的ViewHolder(getTag)中拿到convertView佈局中的控件,省去了findViewById的時間。上面的代碼就是這樣的原理。

  而後就是CommonAdapter了。

複製代碼
public abstract class CommonAdapter<T> extends BaseAdapter {
    
    protected LayoutInflater inflater;
    protected Context context;
    protected List<T> datas;
    protected final int itemLayoutId;
    
    public CommonAdapter(Context context, List<T> datas, int itemLayoutId) {
        this.context = context;
        this.inflater = LayoutInflater.from(context);
        this.datas = datas;
        this.itemLayoutId = itemLayoutId;
    }
    
    @Override
    public int getCount() {
        return datas != null ? datas.size() : 0;
    }
    
    @Override
    public T getItem(int position) {
        return datas.get(position);
    }
    
    @Override
    public long getItemId(int position) {
        return position;
    }
    
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        final CommonViewHolder viewHolder = getViewHolder(position, convertView, parent);
        convert(viewHolder, getItem(position), position);
        return viewHolder.getConvertView();
        
    }

public abstract void convert(CommonViewHolder viewHolder, T item, int position); }
複製代碼

  好了,不貼代碼了。看不明白了請點擊這裏:Android 快速開發系列 打造萬能的ListView GridView 適配器

2.4.5 其它Utils封裝

  如AES128加密類、BitmapUtils、SecurePreferences、StringUtil、ToastUtil、IOUtil等等。

2.5 接口測試

  爲了保證數據交換、加解密正常,首先對某一個接口進行測試,以驗證API Service能正常跑通。好比能夠先對登陸進行模擬測試,看是否成功,同時包括異常的測試,服務端是否是處理了邊界異常,返回給客戶端的都是封裝過的異常信息,而不是拋一個敏感信息給客戶端。提早進行接口測試有助於咱們的基礎組件運行沒問題,方便後期其它模塊的快速集成。

2.6 快速開發

  基礎組件封裝好後,除了少許的從網絡獲取數據邏輯和本地數據庫的增刪改查,客戶端基本上就是界面的佈局工做了。界面開發基本看熟練程度和自定義View的重用。

 

  好了,基本就這些。兩個Android開發人員兩個月內完成確定是能夠的,前提是至少有一個熟手。後面再談談 MVP,畢竟這個客戶端設計無法進行單元測試,若是業務邏輯愈來愈複雜,Activity的職責會愈來愈重,問題多多,不利於後期維護。
相關文章
相關標籤/搜索