Android 開發中有什麼經典的輪子值得自己去實現一遍?

鏡像問題:

前端開發中有什麼經典的輪子值得自己去實現一遍? - 編程 - 知乎


最近在公司主導開發Android中間件,我對中間件的定義是:多個應用都會用到且相對獨立的功能,但不涉及UI的部分,比如:

  • 文件下載庫

  • 文件上傳庫

  • 行為採集庫

  • 更新庫

  • 推送庫

  • 即時通訊庫

  • 賬號庫

  • 資料庫

  • 加密解密庫

  • 網路請求庫

  • 圖片緩存庫

  • 序列化和反序列化庫

  • 日誌庫

  • 通用庫

  • 問題反饋庫

這些庫很多都能夠在開源網站上能夠找到,無論是star數量、持續維護的狀態、效率和性能、包大小、實際項目中的使用情況都是非常棒的,如果純粹是解決開發效率的問題,很多使用github上的開源庫就行了,比如圖片緩存庫:fresco、picasso、glide、UIL;網路請求庫:Okhttp、Volley、Retrofit;序列化和反序列化庫:gson、fastjson;不同功能對應的開源庫可以看這裡:Must Have Libraries。

關於開源庫的選型可以看下這幾個鏈接,但在項目中具體用哪一個還得根據自身業務情況來定:

  • Android開源項目推薦之「網路請求哪家強」

  • Android開源項目推薦之「圖片載入到底哪家強」
  • 國內Top500Android應用分析報告

  • Google Play Top200 應用分析報告

但如果你要根據自身業務定製或者是想提高自己的開發技能和設計能力,很多庫都是值得自己親身去研究研究的,在這個過程中其實提升技能只是一部分,重點還是在於如何設計這個庫去滿足業務的需求以及向前和向後兼容的問題。我隨便寫幾個庫的需求和可能用到的知識點列出來,題主可以根據自身情況試著造造輪子。

文件下載庫:

  1. 開始、暫停、刪除、增加、查詢下載任務;

  2. 支持斷點續傳;

  3. 支持多線程下載;

  4. 重點要考慮多個任務同時下載時的性能問題;

  5. 網路切換時的處理。

文件上傳庫:

  1. 開始、暫停、刪除、增加、查詢上載任務;

  2. 支持斷點續傳;

  3. 支持多線程(分塊)上傳;

  4. 出於性能考慮需要考慮限制文件的大小;

  5. 網路切換時的處理;

行為採集庫:

1、支持整機和單個應用的用戶操作事件的採集並上傳;

2、支持整機和單個應用的異常信息採集並上傳;

3、支持多種採集模式:定時、推送、定量、充電時上傳等上傳模式;

4、採集的緩存策略,需要考慮兩級緩存:內存緩存和磁碟緩存,否則會有功耗的問題;

5、需要考慮到文件上傳的時機(網路訪問的時機),也會涉及到功耗問題。

更新庫:

1、支持全量升級和增量升級;

2、怎樣定升級策略才能保證升級的效率是最高的,比如安裝包小於1M時,可能全量升級的效率比增量升級的效率更高;

3、升級需要考慮到和業務強相關的情況,比如檢測到更新後是先提示用戶還是等安裝包下載完成後提示用戶,這種都不能在庫中寫死,最多提供一種默認的策略,上層應用是可以自定義的;

4、增量升級差分包的管理;

推送庫:

1、支持多種推送策略:全量推送、指定用戶推送、定時推送等;

2、需要考慮到多個應用同時集成推送功能時的功耗問題;

3、推送服務如何保活;

每個庫還需要考慮到下面這些:

  1. 介面的向前兼容和向後兼容問題;

  2. 庫的錯誤碼設計問題;

  3. 庫可能會導致的性能問題(比如效率、功耗等);

  4. 庫大小的問題(盡量小,方便集成);

  5. 庫的設計、文檔、Demo都需要考慮防呆;

最後,說一下針對這個問題我的建議:

  • 不建議擼自定義控制項,意義不大,套路熟悉了其實做起來沒什麼大的收穫;

  • 開發一個庫不難,難的是在於持續維護以及是否能夠滿足業務上的需求及需求變化;

  • 建議結合實際業務,將業務中的通用功能形成庫,而不是純粹為了學習或者提升技能去做和自己工作不相關的事情,比如熱修復,其實現在很多應用的安裝包並不大,更新也非常方便,或者在開發階段提升對應用質量的要求,其實熱修復的使用場景是非常有限的,也並沒有多大的價值,花很多精力在這上面就沒有多大的價值了;
  • 不要為了提升簡歷的質量,別人在github上寫一個庫你也跟著寫一個,寫完之後也不維護,這樣就沒什麼價值了。

當然是自己手動實現一下 Content Provider。

現在 ORM 滿天飛,但是 Content Provider 畢竟是 Google 的親兒子,和 CursorLoader,SyncAdapter 配合起來更加是天衣無縫。為其他應用提供數據你需要content provider,為系統中的 widget 提供數據,也需要 content provider 介面。

很多人可能沒有從來沒有寫過Content Provider,更加視 SQLite 語句如洪水猛獸。 其實也沒什麼難的,只要10分鐘閱讀這篇文章,加上一個周末時間自己手動實踐,你就能打開了一個新的大門! 直接上例子,例如我們要做一個天氣應用。先來個俯瞰圖,我們會完成三個大部分,WeatherContract,WeatherDBHelper,WeatherProvider 和他們對應的測試類,測試類很重要!

Contract 設計表

Contract 字面上是合同的意思。在這裡就是指制定規則,資料庫有哪些表,每個表有哪些列,如何訪問數據。

因為我們要現實多個地方的天氣,每個地方又有多日的天氣,因此我們需要兩個表Weather 和 Location,並且在Weather表中有一個Location_id作為外鍵指向Location表的主鍵

然後天氣表當然還要包含很多我們需要顯示的信息,例如日期,最高溫度,最低溫度,天氣狀況等等。具體代碼可以定義如下:

/* Inner class that defines the contents of the weather table */
public static final class WeatherEntry implements BaseColumns {

public static final String TABLE_NAME = "weather";

// Column with the foreign key into the location table.
public static final String COLUMN_LOC_KEY = "location_id";
// Date, stored as long in milliseconds since the epoch
public static final String COLUMN_DATE = "date";
// Weather id as returned by API, to identify the icon to be used
public static final String COLUMN_WEATHER_ID = "weather_id";

// Short description and long description of the weather, as provided by API.
// e.g "clear" vs "sky is clear".
public static final String COLUMN_SHORT_DESC = "short_desc";

// Min and max temperatures for the day (stored as floats)
public static final String COLUMN_MIN_TEMP = "min";
public static final String COLUMN_MAX_TEMP = "max";

// Humidity is stored as a float representing percentage
public static final String COLUMN_HUMIDITY = "humidity";

// Humidity is stored as a float representing percentage
public static final String COLUMN_PRESSURE = "pressure";

// Windspeed is stored as a float representing windspeed mph
public static final String COLUMN_WIND_SPEED = "wind";

// Degrees are meteorological degrees (e.g, 0 is north, 180 is south). Stored as floats.
public static final String COLUMN_DEGREES = "degrees";
}

/*
Inner class that defines the contents of the location table
*/
public static final class LocationEntry implements BaseColumns {

public static final String TABLE_NAME = "location";

// The location setting string is what will be sent to openweathermap
// as the location query.
public static final String COLUMN_LOCATION_SETTING = "location_setting";

// Human readable location string, provided by the API. Because for styling,
// "Mountain View" is more recognizable than 94043.
public static final String COLUMN_CITY_NAME = "city_name";

// In order to uniquely pinpoint the location on the map when we launch the
// map intent, we store the latitude and longitude as returned by openweathermap.
public static final String COLUMN_COORD_LAT = "coord_lat";
public static final String COLUMN_COORD_LONG = "coord_long";
}
}

WeatherDBHelper 實現

完成了表的設計以後,我們要用SQLiteDbHelper來真正創建資料庫。其實就是寫兩個創建表的SQL語句,然後執行SQL語句。

@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
final String SQL_CREATE_LOCATION_TABLE = "CREATE TABLE " + LocationEntry.TABLE_NAME + " (" +
LocationEntry._ID + " INTEGER PRIMARY KEY," +
LocationEntry.COLUMN_LOCATION_SETTING + " TEXT UNIQUE NOT NULL, " +
LocationEntry.COLUMN_CITY_NAME + " TEXT NOT NULL, " +
LocationEntry.COLUMN_COORD_LAT + " REAL NOT NULL, " +
LocationEntry.COLUMN_COORD_LONG + " REAL NOT NULL " +
" );";

final String SQL_CREATE_WEATHER_TABLE = "CREATE TABLE " + WeatherEntry.TABLE_NAME + " (" +

WeatherEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +

// the ID of the location entry associated with this weather data
WeatherEntry.COLUMN_LOC_KEY + " INTEGER NOT NULL, " +
WeatherEntry.COLUMN_DATE + " INTEGER NOT NULL, " +
WeatherEntry.COLUMN_SHORT_DESC + " TEXT NOT NULL, " +
WeatherEntry.COLUMN_WEATHER_ID + " INTEGER NOT NULL," +

WeatherEntry.COLUMN_MIN_TEMP + " REAL NOT NULL, " +
WeatherEntry.COLUMN_MAX_TEMP + " REAL NOT NULL, " +

WeatherEntry.COLUMN_HUMIDITY + " REAL NOT NULL, " +
WeatherEntry.COLUMN_PRESSURE + " REAL NOT NULL, " +
WeatherEntry.COLUMN_WIND_SPEED + " REAL NOT NULL, " +
WeatherEntry.COLUMN_DEGREES + " REAL NOT NULL, " +

// Set up the location column as a foreign key to location table.
" FOREIGN KEY (" + WeatherEntry.COLUMN_LOC_KEY + ") REFERENCES " +
LocationEntry.TABLE_NAME + " (" + LocationEntry._ID + "), " +

// To assure the application have just one weather entry per day
// per location, it"s created a UNIQUE constraint with REPLACE strategy
" UNIQUE (" + WeatherEntry.COLUMN_DATE + ", " +
WeatherEntry.COLUMN_LOC_KEY + ") ON CONFLICT REPLACE);";

sqLiteDatabase.execSQL(SQL_CREATE_LOCATION_TABLE);
sqLiteDatabase.execSQL(SQL_CREATE_WEATHER_TABLE);
}

TestDb實現

到這裡我們已經可以設計一些測試樣例來測試兩個表是否都成功建立,編寫 ContentValues,測試插入成功,讀取成功,主鍵,副鍵和約束條件是否生效等等。

這裡代碼就省略了。可以在文章最後找到課程地址,查看一步步建立content provider的課程視頻。

Contract 設計URI

設計URI就是設計如何和表中的信息交互,包括讀和寫。作為一個天氣應用,當然我們要獲取

  • 所有天氣,
  • 某個位置的所有天氣,
  • 以及某個位置某個日期的天氣
  • 所有位置,

四種訪問數據的方式。其中第三種返回一個,其他都返回多個。根據這些需求,我們可以定義以下四個 URI 和他們對應的類型。

這裡寫出contract裡面所有代碼太長了,就用Weather Path 下面的三個URI作為例子, 省略了Location Path下面的 URI,和一些輔助函數。

public static final String CONTENT_AUTHORITY = "com.example.android.sunshine.app";
public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
public static final String PATH_WEATHER = "weather";

public static final Uri CONTENT_URI =
BASE_CONTENT_URI.buildUpon().appendPath(PATH_WEATHER).build();
public static final String CONTENT_TYPE =
ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_WEATHER;
public static final String CONTENT_ITEM_TYPE =
ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_WEATHER;

public static Uri buildWeatherUri(long id) {
return ContentUris.withAppendedId(CONTENT_URI, id);
}

public static Uri buildWeatherLocation(String locationSetting) {
return CONTENT_URI.buildUpon()
.appendPath(locationSetting)
.build();
}

public static Uri buildWeatherLocationWithDate(String locationSetting, long date) {
return CONTENT_URI.buildUpon()
.appendPath(locationSetting)
.appendPath(Long.toString(normalizeDate(date)))
.build();
}

Content Provider 重載

萬事俱備,下面開始開始真正重載 ContentProvider,完成我們自己的WeatherProvider了. 我們需要重載下面幾個核心函數。onCreate比較簡單,只需要在裡面創建並獲取資料庫就可以了。但是在開始完成其他五個函數之前,我們先要實現UriMatcher, 因為這五個函數都依賴於UriMatcher 來為每個URI匹配正確的行為。

Content Provider UriMatcher

我們需要用 UriMatcher 來確認進入provider的 URI是哪裡個,從而完成相對應的工作。上面的設計有四個URI,因此我們需要匹配所有四種 URI

static final int WEATHER = 100;
static final int WEATHER_WITH_LOCATION = 101;
static final int WEATHER_WITH_LOCATION_AND_DATE = 102;
static final int LOCATION = 300;

static UriMatcher buildUriMatcher() {
final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
final String authority = WeatherContract.CONTENT_AUTHORITY;

// For each type of URI you want to add, create a corresponding code.
matcher.addURI(authority, WeatherContract.PATH_WEATHER, WEATHER);
matcher.addURI(authority, WeatherContract.PATH_WEATHER + "/*", WEATHER_WITH_LOCATION);
matcher.addURI(authority, WeatherContract.PATH_WEATHER + "/*/#", WEATHER_WITH_LOCATION_AND_DATE);

matcher.addURI(authority, WeatherContract.PATH_LOCATION, LOCATION);
return matcher;
}

這裡也是個絕佳的時間添加一些 URIMatcher 的測試代碼來確認是否每個uri 都已經被正確得匹配。 這會節省你很多很多的調試的時間。

Content Provider getType()

getType是剩餘五個函數裡面最簡單的,匹配uri以後,只要返回對應的type就可以了。對於其他的query,insert,update,delete 我們也採取同樣的 switch case語句,只不過每個情況都稍微複雜一點罷了。

@Override
public String getType(Uri uri) {

// Use the Uri Matcher to determine what kind of URI this is.
final int match = sUriMatcher.match(uri);

switch (match) {
// Student: Uncomment and fill out these two cases
case WEATHER_WITH_LOCATION_AND_DATE:
return WeatherContract.WeatherEntry.CONTENT_ITEM_TYPE;
case WEATHER_WITH_LOCATION:
return WeatherContract.WeatherEntry.CONTENT_TYPE;
case WEATHER:
return WeatherContract.WeatherEntry.CONTENT_TYPE;
case LOCATION:
return WeatherContract.LocationEntry.CONTENT_TYPE;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}

Content Provider query()

query 是最複雜的,基本結構當然和 getType一樣,但是就像之前說的,每個情況都略微複雜。如果要獲得某個日期,某個地點的天氣,我們就需要把兩個表join起來才可以。因此我們定義一個比較複雜的from語句,和一個比較複雜的where語句。我們就以最複雜的 WEATHER_WITH_LOCATION_AND_DATE 為例

case WEATHER_WITH_LOCATION_AND_DATE:
{

sWeatherByLocationSettingQueryBuilder = new SQLiteQueryBuilder();

//FROM clause
//This is an inner join which looks like
//weather INNER JOIN location ON weather.location_id = location._id
sWeatherByLocationSettingQueryBuilder.setTables(
WeatherContract.WeatherEntry.TABLE_NAME + " INNER JOIN " +
WeatherContract.LocationEntry.TABLE_NAME +
" ON " + WeatherContract.WeatherEntry.TABLE_NAME +
"." + WeatherContract.WeatherEntry.COLUMN_LOC_KEY +
" = " + WeatherContract.LocationEntry.TABLE_NAME +
"." + WeatherContract.LocationEntry._ID);

//WHERE clause
String sLocationSettingAndDaySelection =
WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING + " = ?"
+ " AND " + WeatherContract.WeatherEntry.COLUMN_DATE + " = ?";

//WHER clause argument
String locationSetting = WeatherContract.WeatherEntry.getLocationSettingFromUri(uri);
long date = WeatherContract.WeatherEntry.getDateFromUri(uri);

//actual SQL query statement
retCursor = sWeatherByLocationSettingQueryBuilder.query(mOpenHelper.getReadableDatabase(),
projection,
sLocationSettingAndDaySelection,
new String[]{locationSetting, Long.toString(date)},
null,
null,
sortOrder
);
break;
}
retCursor.setNotificationUri(getContext().getContentResolver(), uri);

return retCursor

Content Provider update(), insert(), delete()

update insert delete 相對比較簡單,因為我們只需要針對 WEATHER 和 LOCATION 兩種URI進行操作就可以了。需要注意的是,在對資料庫進行修改了以後,應該調用

getContext().getContentResolver().notifyChange(uri, null);

來通知所有的observer這個URI內容變化了,loader的功效在這裡也就體現出來了。這裡就只給出insert的代碼,其他兩個的類似

@Override
public Uri insert(Uri uri, ContentValues values) {
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
final int match = sUriMatcher.match(uri);
Uri returnUri;

switch (match) {
case WEATHER: {
normalizeDate(values);
long _id = db.insert(WeatherContract.WeatherEntry.TABLE_NAME, null, values);
if ( _id &> 0 )
returnUri = WeatherContract.WeatherEntry.buildWeatherUri(_id);
else
throw new android.database.SQLException("Failed to insert row into " + uri);
break;
}
case LOCATION: {
long _id = db.insert(WeatherContract.LocationEntry.TABLE_NAME, null, values);
if ( _id &> 0 )
returnUri = WeatherContract.LocationEntry.buildLocationUri(_id);
else
throw new android.database.SQLException("Failed to insert row into " + uri);
break;
}
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return returnUri;
}

至此整個content provider也就實現完了。當然每完成一個函數,我們都應該添加一些單元測試代碼來驗證這個函數是否正常工作。這屬於一個比較複雜的content provider,但其實真的寫出來就沒多少嘛 (-_-)花這點時間,絕對是一個有價值的投資!

以上內容都來自於我們和Google共同開發的免費課程 Android應用開發。


網路庫,圖片庫,照著開源的擼一遍,然後在用你擼的這些庫,結合rxjava mvp擼一個微博客戶端。


推薦閱讀:

做用戶界面設計,1920*1080 解析度智能電視的 SafeZone 應該是多大?
安卓手機上有哪些應用可以開發軟體?
如何在 Android 手機上實現抓包?
安卓平台遊戲用戶的最大痛點是什麼?
電子信息工程專業學生去學android開發靠譜嗎?

TAG:編程 | Android開發 | GitHub | Android | 開源社區 |