1. 程式人生 > >一個網際網路app的開發設計(技術選型和架構)

一個網際網路app的開發設計(技術選型和架構)

               在做一個網際網路應用時, 要考慮技術選型和架構搭建。 先說說技術選型,   以丁丁租房為例在開發時會面對如下問題:

1、圖片處理, image loader、picasso、Glide、Fresco, 推薦使用fresco,因為它使用三級快取、佔記憶體更小;

2、http通訊,  開源框架有很多例如volley,retrofit,okhttp等等,    用法都很簡單也類似,  推薦使用OkHttp,它支援SPDY;

3、崩潰日誌採集, 免費庫也有很多,  騰訊bugly、友盟、Fabric。我們使用的是Fabric,  挺好用的。

4、即時聊天,   三方庫也有很多, 就不多說了。 我們用的是leancloud, 因為它免費:), 而且效果還可以。

5、控制元件,   根據UI需要可以自己寫或用別人寫好的三方庫, 例如Materialdialog,MPAndroidChart, WheelView等等。

6、程序內部訊息傳遞,   例如跟Service互動的binder、或者LocalbroadcastReceiver(基於主執行緒handler實現的,所以不會被其他程序收到,比較安全!)、三方庫EventBus和觀察者模式等等。

7、支付, 可以用支付寶、微信的介面。 

8、埋點, 這個就很重要了,  產品經理每天都在盯著這個統計資料, 用他們的話叫做資料引導決策,  有時前端也會弄ABTest, 目的是做出讓使用者更喜歡的東西。 我們用了Countly, 因為它開源,  後臺可以自己做,  埋點資料可以傳到自己的伺服器。 畢竟一些敏感的資料是不想讓三方如友盟知道的。

9、bug熱修復,  目前有很多技術方案, 我們參考了HotFix的方式在專案中落地,  網上有人說適配有問題, 但我們還沒碰到。。。

10、外掛化,  應用典型案例就是支付寶、微信。當app功能非常多、程式碼量很大(超過65535個方法)時要考慮,  我們是計劃實踐一下, 但因為公司突然倒閉, 沒來得及做。

11、程式框架,   對比mvc、mvp和mvvm模式, 我覺得mvp模式比較合適並落地到丁丁租房app裡, 因為mvp很好的實現了程式碼解耦、邏輯分層,  下層對上層透明, 每層只關心自己那點事情就夠了。

12、UI標註,推薦使用標你妹啊網站、app.zeplin.io網站。

                                   下面丁丁租房架構圖, 最上面一層是各個功能。

   程式碼結構如下, 按照業務劃分一級目錄, 二級目錄是按照android各元件區分。


      從二級目錄看到activity例項化presenter並儲存該引用, presenter通過Activity傳進來的interface回撥到Activity, presenter實現網路介面。 

       除錯網路介面要說一下, 在Activity/Fragment退出時要取消未完成的網路請求, 避免耗費流量和回撥刷ui時的異常; 可以用RestClient在瀏覽器測試介面,目的是理解和除錯介面的有效性;    

下面附乾貨原始碼, 供參考:

Model程式碼:

/** * http網路相關工具類, 目前使用三方庫OkHttp庫
 */
public class NetworkUtils {

    //網路介面執行週期打點
private static class DotNet {
        public String tag;   //網路介面的標籤
public long   timestamp; //時間戳
public String uuid;
}

    private static OkHttpClient sInstance;
//初始化OkHttp例項
static {
        sInstance = new OkHttpClient();
sInstance.setConnectTimeout(10, TimeUnit.SECONDS);  //連線超時時間
sInstance.setWriteTimeout(10, TimeUnit.SECONDS);    //上傳檔案超時時間
sInstance.setReadTimeout(20, TimeUnit.SECONDS);     //下載檔案超時時間
}

    /**
     * 格式化請求body
     *
     * @param object, 查詢用的引數,Map or entity
     */
private static RequestBody formatRequestBody(
            Context context,
Object object) {

        //JSON.toJSONString(object)方法預設執行去除value為null的欄位
        /*Iterator iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry) iterator.next();
            //編輯房源時可以傳空字串
            if (entry.getValue() == null) {
                iterator.remove();
            }
        }*/
LinkedHashMap baseMap = GatewayUtils.getInstance().getBase(context);  //獲取base引數
Iterator it = baseMap.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry) it.next();
            if (entry.getValue() == null || StringUtils.isNull(entry.getValue().toString())) {
                it.remove();
}
        }

        String param = JSON.toJSONString(object);           //生成param引數
String sign = ParamBuild.getSign(object, baseMap);  //計算sign引數
baseMap.put("sign", sign);
String base = JSON.toJSONString(baseMap);
RequestBody formBody = new FormEncodingBuilder()
                .add("base", base)
                .add("param", param)
                .build();
        return formBody;
}

    /**
     * 格式化請求body
     *
     * @param object, 查詢用的引數,Map or entity
     * @param dotNet, 打點要用的引數
     */
private static RequestBody formatRequestBodyExt(
            Context context,
Object object,
DotNet dotNet) {

        //JSON.toJSONString(object)方法預設執行去除value為null的欄位
        /*Iterator iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry) iterator.next();
            //編輯房源時可以傳空字串
            if (entry.getValue() == null) {
                iterator.remove();
            }
        }*/
LinkedHashMap baseMap = GatewayUtils.getInstance().getBase(context);  //獲取base引數
Iterator it = baseMap.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry) it.next();
            if (entry.getValue() == null || StringUtils.isNull(entry.getValue().toString())) {
                it.remove();
}
        }

        try {
            //網路介面執行週期打點時的引數
String uuid = (String) baseMap.get("uuid");
dotNet.timestamp = System.currentTimeMillis();
dotNet.uuid = uuid;
} catch (Exception ex) {
            ex.printStackTrace();
}

        String param = JSON.toJSONString(object);           //生成param引數
String sign = ParamBuild.getSign(object, baseMap);  //計算sign引數
baseMap.put("sign", sign);
String base = JSON.toJSONString(baseMap);
RequestBody formBody = new FormEncodingBuilder()
                .add("base", base)
                .add("param", param)
                .build();
        return formBody;
}



    /**
     * 與伺服器非同步互動, 並返回結果字串
     *
     * @param ctx      應用上下文
     * @param url      伺服器url地址
     * @param map      查詢引數
     * @param tag      標籤,用於取消請求
     * @param listener 回撥
     */
public static void asyncWithServer(final Context ctx,
String url,
Map map,
String tag,
                                       final OnNetworkListener listener) {
        if (ctx == null
|| url == null
|| listener == null
|| tag == null
|| map == null
) {
            return;
}

        DotNet dotNet = new DotNet();
dotNet.tag = tag;
        final RequestBody requestBody = formatRequestBodyExt(ctx, map, dotNet);   //組織請求包體
Request request = new Request.Builder()
                .tag(dotNet)
                .url(url)
                .addHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")
                .post(requestBody)
                .build();
sInstance.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Request request, IOException e) {
                DotNet dotTag = (DotNet)request.tag();
listener.onFailure(dotTag.tag);
}

            @Override
            public void onResponse(Response response) throws IOException {
                DotNet dotTag = (DotNet)response.request().tag();
                if (!response.isSuccessful()) {
                    //失敗
listener.onFailure(dotTag.tag);
} else {
                    //Countly打點, 統計介面執行週期
try {
                        dotInterfaceTag(ctx, dotTag.timestamp, dotTag.uuid,
response.request().urlString());
} catch (Exception ex) {
                        ex.printStackTrace();
}

                    String jsonBody = response.body().string();
//新增json解析異常邏輯,https://fabric.io/cb5ab5/android/apps/com.dingding.client/issues/561cf641f5d3a7f76bc3e0df
Map<String, Object> map = null;
                    try {
                        map = JSON.parseObject(jsonBody);
} catch (JSONException ex) {
                        ex.printStackTrace();
} finally {
                        if (map == null
|| map.size() == 0) {
                            listener.onFailure(dotTag.tag); //解析失敗時按照介面失敗處理
return;
}
                    }
                    //更新token, 可能引起效能問題, 相當於做了2次json解析
if (map.containsKey("token")) {
                        String token = (String) map.get("token");
GatewayUtils.getInstance().setToken(ctx, token);
}
                    Integer code = (Integer) map.get("code");
                    if (code == 100016) {//code為100016時為token過期 重新登入
GatewayUtils.getInstance().setDefult(ctx);
Intent intent = new Intent();
intent.setAction("com.dingding.client.NewLoginActivity");
intent.putExtra("code", code);
intent.putExtra("from", "2");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ctx.startActivity(intent);
}
                    listener.onSuccess(jsonBody,
dotTag.tag);
}

            }
        });
}

    /**
     * 與伺服器非同步互動, 並返回類物件
     *
     * @param ctx      應用上下文
     * @param url      伺服器url地址
     * @param map      查詢引數
     * @param tag      標籤,用於取消請求
     * @param listener 回撥
     * @param clz,     解析結果類 可以傳入null  null是為了考慮有些返回值沒有data節點
     * @param isList   ,傳入true則返回解析為列表,傳入false則返回為物件
     */
public static void asyncWithServerExt(Context ctx,
String url,
Map map,
String tag,
                                          final OnNetworkListener listener,
                                          final Class<?> clz,
                                          final boolean isList) {
        asyncWithServerExt(ctx, url, map, tag, listener, clz, isList, null);
}


    /**
     * 與伺服器非同步互動, 並返回類物件
     *
     * @param ctx      應用上下文
     * @param url      伺服器url地址
     * @param object      查詢引數,Map or entity
     * @param tag      標籤,用於取消請求
     * @param listener 回撥
     * @param clz,     解析結果類 可以傳入null  null是為了考慮有些返回值沒有data節點
     * @param isList   ,傳入true則返回解析為列表,傳入false則返回為物件
     */
public static void asyncWithServerExt(final Context ctx,
String url,
Object object,
String tag,
                                          final OnNetworkListener listener,
                                          final Class<?> clz,
                                          final boolean isList, final String key) {
        if (ctx == null
|| url == null
|| listener == null
|| tag == null
|| object == null
) {
            return;
}

        DotNet dotNet = new DotNet();
dotNet.tag = tag;
RequestBody requestBody = formatRequestBodyExt(ctx, object, dotNet);   //組織請求包體, 會修改時間戳和uuid
Request request = new Request.Builder()
                .tag(dotNet)
                .url(url)
                .addHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")
                .post(requestBody)
                .build();
sInstance.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Request request, IOException e) {
                DotNet dotNet = (DotNet) request.tag();
listener.onFailure(dotNet.tag);
}

            @Override
            public void onResponse(Response response) throws IOException {
                DotNet dotTag = (DotNet) response.request().tag();
                if (!response.isSuccessful()) {
                    //失敗
listener.onFailure(dotTag.tag);
} else {
                    //Countly打點, 統計介面執行週期
try {
                        dotInterfaceTag(ctx, dotTag.timestamp, dotTag.uuid,
response.request().urlString());
} catch (Exception ex) {
                        ex.printStackTrace();
}

                    //json解析並回調
DataFactory factory = new ResultObjectFactory();
ResultObject result;
                    if (clz == null) {
                        result = factory.createResultObjectNoCls(ctx, response.body().string());
} else {
                        result = factory.createResultObject(ctx, isList,
response.body().string(),
clz, key);
}
                    if (result == null) {
                        listener.onFailure(dotTag.tag);
} else {
                        listener.onSuccessExt(result, dotTag.tag);
}
                }

            }
        });
}

    /**
     * Countly打點計算每個介面的執行週期
     * @param url, 請求的url
     */
private static void dotInterfaceTag(Context ctx, long timestamp, String uuid, String url) {
        long diff = Math.abs(System.currentTimeMillis() - timestamp);
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("uuid", uuid);
paramMap.put("time", diff + "");
paramMap.put("url", url);
Log.d("NetPerformance", "url:" + url + ", diff:" + diff);
Statistics.dotNetPerformance(ctx, paramMap);
}


    /**
     * 與伺服器非同步互動, 並返回類物件
     *
     * @param ctx      應用上下文
     * @param url      伺服器url地址
     * @param map      查詢引數
     * @param tag      標籤,用於取消請求
     * @param listener 回撥
     * @param clz,     解析結果類  可以傳null
     */
public static void asyncWithServerExt(Context ctx,
String url,
Map map,
String tag,
                                          final OnNetworkListener listener,
                                          final Class<?> clz) {
        asyncWithServerExt(ctx, url, map, tag, listener, clz, false);
}

    /**
     * 與伺服器同步互動, 必須在非UI執行緒執行!
     */
public static String syncWithServer(
            Context ctx,
String url,
Map map) {
        if (ctx == null
|| url == null
|| map == null) {
            return "";
}

        String retStr = null;    //伺服器返回的包體
RequestBody requestBody = formatRequestBody(ctx, map);   //組織請求包體
Request request = new Request.Builder()
                .url(url)
                .addHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")
                .post(requestBody)
                .build();
Response response;
        try {
            response = sInstance.newCall(request).execute();
            if (response != null
&& response.isSuccessful()) {
                retStr = response.body().string();  //包體
}
        } catch (IOException ex) {
            ex.printStackTrace();
} finally {

        }

        return retStr;
}

    //返回ResultObject
public static ResultObject syncWithServer(
            Context ctx,
String url,
Map map,
            final Class<?> clz) {

        RequestBody requestBody = formatRequestBody(ctx, map);   //組織請求包體
Request request = new Request.Builder()
                .url(url)
                .addHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")
                .post(requestBody)
                .build();
ResultObject result;
Response response;
DataFactory factory;
        try {
            response = sInstance.newCall(request).execute();
factory = new ResultObjectFactory();
result = factory.createResultObject(ctx, false, response.body().string(), clz, null);
} catch (IOException ex) {
            result = new ResultObject();
result.setCode(-1);
result.setSuccess(false);
result.setMessage("網路錯誤");
}

        return result;
}

Presenter程式碼:

/** * Presenter基類
 */
public abstract class BasePresenter {
    public HashMap<String, Object> mKeyMap
            = new HashMap<String, Object>();  //請求的引數
public HashMap<String, Object> mFilterMap
            = new HashMap<String, Object>();   //請求的引數
public Handler mHandler = new Handler();
/**
     * UI主執行緒handler, 用於更新view
     */
protected HashSet<String> mTagList = new HashSet<String>();
    public IBaseView mIView;
/**
     * 與後臺互動時的引數
     *
     * @return
*/
public abstract HashMap<String, Object> getParams();
/**
     * 獲取應用上下文
     */
public abstract Context getContext();
/**
     * 設定應用上下文, 使用ApplicationContext
     */
public abstract void setContext(Context ctx);
/**
     * 設定tag, 用於刪除佇列中的網路請求, 也可以區分不同的介面
     *
     * @param tag
     */
public abstract void setTag(String tag);
/**
     * 獲取tag, 用於刪除佇列中的網路請求,也可以區分不同的介面
     */
public abstract String getTag();
/**
     * 獲取回撥函式的引用, 不同業務的邏輯不同
     */
public abstract OnNetworkListener getListener();
/**
     * 設定View引用,派生類可以擴充套件IBaseView介面類, 在activity裡實例化,並通過強制類轉換賦值。
     */
public void setView(IBaseView view) {
        mIView = view;
}

    /**
     * 獲取View引用, 派生類可以擴充套件IBaseView介面類,在activity裡實例化,並通過強制類轉換賦值。
     */
public IBaseView getView() {
        return mIView;
}

    /**
     * 在ui主執行緒執行更新介面的操作
     */
public void updateUI(Runnable runnable) {
        mHandler.post(runnable);
}

    /**
     * 清空引數
     */
public void resetParams() {
        mKeyMap.clear();
mFilterMap.clear();
}

    /**
     * 與後臺互動, 派生類可以例項化回撥函式OnNetworkListener。
     * 回撥函式會執行onSuccess(String body)
     *
     * @param url,      網址
     * @param listener, model層的回撥。 為空時使用getListener()
     * @return boolean, 是否成功傳遞到model層
     */
protected boolean asyncWithServer(String url, OnNetworkListener listener) {
        String tag = getTag();
Context ctx = getContext();
OnNetworkListener callback;
        if (listener == null) {
            callback = getListener();
} else {
            callback = listener;
}

        if (tag == null
|| callback == null
|| ctx == null) {
            return false;
}

        mTagList.add(tag);
NetworkUtils.asyncWithServer(
                ctx,
url,
getParams(),
tag,
callback);
        return true;
}

    /**
     * 與後臺互動, 派生類可以例項化回撥函式OnNetworkListener。
     * 回撥函式會執行onSuccess(ResultObject result)
     *
     * @param url,      網址
     * @param clz,      解析的類名
     * @param listener, model層的回撥。 為空時使用getListener()
     * @return boolean, 是否成功傳遞到model層
     */
protected boolean asyncWithServerExt(String url,
Class<?> clz,
OnNetworkListener listener) {
        return asyncWithServerExt(url, clz, listener, false);
}

    /**
     * 與後臺互動, 派生類可以例項化回撥函式OnNetworkListener。
     * 回撥函式會執行onSuccess(ResultObject result)
     *
     * @param url,      網址
     * @param clz,      解析的類名
     * @param listener, model層的回撥。 為空時使用getListener()
     * @param isList    ,傳入true則返回解析為列表,傳入false則返回為物件
     * @return boolean, 是否成功傳遞到model層
     */
protected boolean asyncWithServerExt(String url,
Class<?> clz,
OnNetworkListener listener,
                                         final boolean isList) {
        return asyncWithServerExt(url, clz, listener, isList, null);
}

    /**
     * 與後臺互動, 派生類可以例項化回撥函式OnNetworkListener。
     * 回撥函式會執行onSuccess(ResultObject result)
     *
     * @param url,      網址
     * @param clz,      解析的類名
     * @param listener, model層的回撥。 為空時使用getListener()
     * @param isList    ,傳入true則返回解析為列表,傳入false則返回為物件
     * @return boolean, 是否成功傳遞到model層
     */
protected boolean asyncWithServerExt(String url,
Class<?> clz,
OnNetworkListener listener,
                                         final boolean isList, final String key) {
        String tag = getTag();
Context ctx = getContext();
OnNetworkListener callback;
        if (listener == null) {
            callback = getListener();
} else {
            callback = listener;
}

        if (tag == null
|| callback == null
|| ctx == null) {
            return false;
}

        mTagList.add(tag);
NetworkUtils.asyncWithServerExt(
                ctx,
url,
getParams(),
tag,
callback,
clz,
isList, key);
        return true;
}

    /**
     * 釋放當前presenter的請求
     * 在activity或fragment的onDestory函式中呼叫
     */
public void cancelRequests() {
        for (Iterator<String> iterator = mTagList.iterator(); iterator.hasNext(); ) {
            String tag = iterator.next();
NetworkUtils.cancelRequestByTag(tag);
}
    }