前一段时间看了包建强老师的《App研发录》,决定将自己写的北京地铁换乘App重构一下,并更名为”帝都地铁”。
此版本将与业务无关的逻辑封装成subwaylib类库,并手写了”网络请求”和”图片加载”模块,优化了”线路搜索”的代码,个人觉得代码质量还是OK的。
Github地址:帝都地铁源码
发起网络请求的代码示例(金山词霸的每日一句Api):
final AppRequestCallback callback = new AppRequestCallback() {
@Override
public void onSuccess(String content) {
Sentence sentence = JSON.parseObject(content, Sentence.class);
if (sentence != null) {
ImageLoader.getInstance().displayImage(sentence.getPicture(), ivPicture);
tvContent.setText(sentence.getContent());
tvNote.setText(sentence.getNote());
}
}
};
AppHttpRequest.getInstance().performRequest(this, "dsapi", null, callback);
项目中所有的网络请求的配置信息都写在本地xml文件中:
AppHttpRequest执行请求时,会根据Key值取出网络请求的配置信息,包括:缓存时间(Expires),请求方式(NetType),模拟数据(MockClass),接口地址(Url)。
final URLData urlData = UrlConfigManager.findURL(activity, apiKey);
模拟数据不为空,则进行本地测试,为空则RequestManager创建Request,RequestThreadPool执行Request。
Request request = activity.getRequestManager().createRequest(urlData, params, callback);
RequestThreadPool.getInstance().execute(request);
Request是一个实现Runnable的抽象类,包含三个抽象方法:
protected abstract void doGet();
protected abstract void doPost();
protected abstract void abort();
分别以HttpClient和HttpURLConnection方式实现了Request,项目中默认使用HttpURLConnection方式获取网络请求数据。
public Request createRequest(final URLData urlData, final List parameters, final RequestCallback requestCallback) {
final Request request = new HurlRequest(urlData, parameters, requestCallback);
addRequest(request);
return request;
}
当以Get方式请求数据时,如果参数不为空,则先拼接参数:
if ((mParameters != null) && (mParameters.size() > 0)) {
mUrl = mUrl + HOST_PARAMS_SEPARATOR + formatRequestParams();
}
如果缓存时间大于0,则取缓存数据:
if (mExpires > 0) {
strCacheContent = CacheManager.getInstance().getFileCache(mUrl);
}
如果缓存数据不为空,则返回缓存数据,为空则创建HttpURLConnection连接,获取接口数据,并将数据写入缓存。
if (urlConn.getResponseCode() == HttpURLConnection.HTTP_OK) {
// 保存Coocie
storeCookie();
// 获取返回的数据
is = urlConn.getInputStream();
String response = BaseUtils.InputStream2String(is);
is.close();
// 把成功获取到的数据记录到缓存
if (mExpires > 0) {
CacheManager.getInstance().putFileCache(mUrl, response, mExpires);
}
// 处理返回信息
doResponse(response);
}
以上即为执行一次网络请求的流程。
加载网络图片的代码示例:
ImageLoader.getInstance().displayImage(sentence.getPicture(), ivPicture);
ImageLoader中会分别尝试从内存和硬盘中获取Bitmap,如果取不到则执行ImageRequest:
public void displayImage(final String imageUrl, final ImageViewWrapper imageViewWrapper, final ImageLoadingListener listener) {
Bitmap bitmap;
if (mImageCache != null) {
// 从内存中获取Bitmap
bitmap = mImageCache.getBitmapFromMemCache(imageUrl);
if (bitmap != null) {
imageViewWrapper.setImageBitmap(bitmap);
return;
}
// 从硬盘中获取Bitmap
bitmap = mImageCache.getBitmapFromDiskCache(imageUrl);[http://](http://)
if (bitmap != null) {
mImageCache.addBitmapToMemCache(imageUrl, bitmap);
imageViewWrapper.setImageBitmap(bitmap);
return;
}
}
// 网络请求Bitmap
ImageRequest request = new ImageRequest(imageUrl, imageViewWrapper, listener, mImageCache);
ImageThreadPool.getInstance().submit(request);
}
ImageRequest实现了Runnable接口,使用HttpURLConnection下载网络图片,并对图片进行inSampleSize处理。开发过程中遇到了这样一个问题:HttpURLConnection获取的InputStream只能被BitmapFactory.decodeStream处理一次:
BitmapFactory.decodeStream returning null when options are set
解决方案是先将InputStream转换为ByteArrayOutputStream,当使用时在转回为InputStream:
// 将InputStream转换为ByteArrayOutputStream
baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) > -1) {
baos.write(buffer, 0, len);
}
baos.flush();
final BitmapFactory.Options options = BitmapDecoder.getBitmapFactoryOptions(
new ByteArrayInputStream(baos.toByteArray()), mImageViewWrapper.getWidth(), mImageViewWrapper.getHeight()
);
final Bitmap bitmap = BitmapDecoder.decodeBitmapFromInputStream(new ByteArrayInputStream(baos.toByteArray()), options);
顺便再说一个小插曲,测试图片缓存的时候,需要断网来测试图片缓存和加载。有一次发现图片加载总是失败,debug一顿找,发现connect时总是异常,顿时不觉明历,最后恍然醒悟,wifi让我关了没开,哎,程序员真是苦啊。
本期将线路搜索的代码都写在SubwayMap类中,这样看起来也更直观。
首先,分别获取起点终点车站的车站ID集合:
// 起点车站名对应的车站ID集合
List lstFromStationIds = mStationDao.getStationIdsByStationName(fromStationName);
// 终点车站名对应的车站ID集合
List lstToStationIds = mStationDao.getStationIdsByStationName(toStationName);
这里要说明一下,比如说军事博物馆站是一个换乘车站,它有两个车站ID,分别为0109和0904,车站ID前两位表示车站所在线路,即军事博物馆站属于1号线和9号线,当以军事博物馆站为起点站查询时需要考虑分别从1号线和9号线为起点线路向其他线路换乘,因此需要取出车站名对应的车站ID集合,再根据车站ID集合获取起点终点线路集合。
接着,获取起点终点线路集合,比如从丰台科技园到北京站,则[09,02]:
List lstFromToLineIds = getFromToLineIds(lstFromStationIds, lstToStationIds);
接着,获取起点到终点换乘路线详细信息,[09,01,02],[09,06,02],[09,04,02]:
List lstTransferRouteLineIds = getFromToTransferRouteLineIds(lstFromToLineIds);
接着,遍历起点到终点换乘路线详细信息,以此加载换乘数据,并获取换乘详细信息:
TransferDetail transferDetail = new TransferDetail();
transferDetail.fromStationName = mFromStationName;
transferDetail.toStationName = mToStationName;
transferDetail.lstTransferRoute = new ArrayList<>();
for (String[] lids : lstTransferRouteLineIds) {
// 构建临接表建图
createSubwayMap(lids, lstFromStationIds.get(0), lstToStationIds.get(0));
// 从图中搜索两点之间最短距离
TransferRoute transferRoute = searchTransferRoute(lstFromStationIds.get(0), lstToStationIds.get(0));
// 添加换乘路线
updateTransferDetail(transferDetail, transferRoute);
}
这里也要说明一下,军事博物馆有两个车站ID,但构建地铁图时只能使用唯一的一个,因此选择车站ID的最小值为图中的车站ID。
最后,返回换乘详情。
transferDetail.ticketPrice = transferDetail.lstTransferRoute.get(0).ticketPrice;
return transferDetail;
其中搜索换乘路线详细信息时用到了深度优先搜索算法:
private void DFS(final int from, final int to) {
if (SubwayData.LINE_TRANSFERS[from][to] == 1) {
int i = 0;
String[] lineIds = new String[mStack.size() + 2];
for (int index : mStack) {
lineIds[i++] = SubwayData.LINE_EDGES[index];
}
lineIds[i++] = SubwayData.LINE_EDGES[from];
lineIds[i] = SubwayData.LINE_EDGES[to];
mLstTransferRouteLineIds.add(lineIds);
} else {
mStack.push(from);
isVisited[from] = true;
for (int i = 0; i < SubwayData.LINE_EDGES.length; i++) {
if (!isVisited[i] && SubwayData.LINE_TRANSFERS[from][i] == 1 && mStack.size() < mMinTransferTimes) {
DFS(i, to);
}
}
isVisited[from] = false;
mStack.pop();
}
}
搜索两点之间最短距离用到了迪杰斯特拉算法:
int cur = 0, min, tmp;
while (!visited[toStationIndex]) {
// 寻找当前最小的路径
min = Integer.MAX_VALUE;
for (int i = 0; i < size; i++) {
if (!visited[i] && distance[i] < min) {
min = distance[i];
cur = i;
}
}
// 标记已获取到最短路径
visited[cur] = true;
// 修正当前最短路径和前驱结点
for (int i = 0; i < size; i++) {
tmp = getFromToDistanceByHeadIndex(cur, i);
if (tmp != Integer.MAX_VALUE) {
tmp += min;
}
if (!visited[i] && tmp < distance[i]) {
distance[i] = tmp;
previous[i] = cur;
}
}
}
以上即为整个项目的大致介绍,具体请看源码。