最近用到七牛上傳視頻和圖片的功能,于是去七牛官網(wǎng)看了文檔,寫(xiě)了一個(gè)上傳文件到七牛的demo,順便將寫(xiě)的過(guò)程中踩的一些坑記錄下來(lái)。
注冊(cè)七牛開(kāi)發(fā)者賬號(hào)
這個(gè)就不說(shuō)了,非常簡(jiǎn)單,注冊(cè)完之后,在左邊對(duì)象存儲(chǔ)項(xiàng)下新建一個(gè)存儲(chǔ)空間,填寫(xiě)存儲(chǔ)空間的名字,其他選項(xiàng)直接默認(rèn)就好了。

然后在左側(cè)個(gè)人中心-密鑰管理下查看自己的AccessKey和SecretKey,應(yīng)該很長(zhǎng)一串字母和數(shù)字組合,說(shuō)到這里不得不提一下我的坑。
我一開(kāi)始用的qq瀏覽器打開(kāi)的這個(gè)網(wǎng)頁(yè),可能是瀏覽器記住了我登錄的賬號(hào)密碼,然后不知道怎么回事將這里的兩個(gè)密鑰替換成了我的賬號(hào)密碼,我以為密鑰就是這個(gè),結(jié)果導(dǎo)致后面的token怎么都不對(duì),調(diào)試了半天總是bad token的錯(cuò)誤,整個(gè)人都不好了。所以能用谷歌瀏覽器還是用谷歌吧。
七牛云的任務(wù)就結(jié)束了,還是很簡(jiǎn)單的,只要?jiǎng)?chuàng)建一個(gè)存儲(chǔ)空間就好了。
獲得token
這一步是非常重要的,先不說(shuō)這個(gè)token是怎么得到的,因?yàn)樗惴ㄓ悬c(diǎn)復(fù)雜。我們直接用官網(wǎng)提供的工具先生成token供后面測(cè)試。最后會(huì)給出生成token的代碼。
點(diǎn)擊左上角的運(yùn)行

會(huì)在右下角看到這個(gè)

上一步我們已經(jīng)看到了兩個(gè)密鑰,將其分別輸入到AccessKey和SecretKey中,bucketname就是之前新建的那個(gè)存儲(chǔ)空間的名字,deadline為這個(gè)token的失效時(shí)間,其他可以不填,再點(diǎn)擊生成uptoken,就會(huì)根據(jù)這些內(nèi)容生成一個(gè)能供你測(cè)試的token。
這里注意一下你設(shè)置的token的有效時(shí)間,這里踩的一個(gè)坑是測(cè)試的時(shí)間太長(zhǎng),1個(gè)小時(shí)后token就失效了,所以自然上傳也會(huì)出錯(cuò),所以當(dāng)你碰到error:null body這個(gè)錯(cuò)誤時(shí)可以試試查看是不是token失效了
安卓端代碼
添加依賴(lài)
這里直接添加比較新的7.3.x,后面會(huì)說(shuō)為什么
compile 'com.qiniu:qiniu-android-sdk:7.3.2'
發(fā)送請(qǐng)求
首先看看文檔是怎么寫(xiě)的

要準(zhǔn)備3個(gè)內(nèi)容,data、key、token
- token ,這個(gè)是最簡(jiǎn)單的,上一步生成的token直接可以拿過(guò)來(lái)用
- key , 這個(gè)是指定你的圖片或其他文件上傳到七牛存儲(chǔ)空間后叫什么名字
- data , 這個(gè)是要上傳的目標(biāo),可以是File類(lèi)型的文件,可以是String類(lèi)型的文件所在目錄,也可是是byte[]數(shù)組,上傳圖片的話肯定是用前兩種比較方便
回調(diào)函數(shù)的參數(shù)文檔中也有

在調(diào)試的時(shí)候可以將info打印到日志中,這樣如果沒(méi)有上傳成功也可以看到是什么原因。如果不主動(dòng)打印日志,那么上傳失敗,AS是不會(huì)打印任何錯(cuò)誤信息的。
這里以上傳手機(jī)中的一張圖片為例,圖片位置在手機(jī)sd卡目錄下test.jpg??煞抡瘴臋n寫(xiě)出以下代碼:
new Thread(new Runnable() {
@Override
public void run() {
UploadManager uploadManager = new UploadManager();
String path = Environment.getExternalStorageDirectory() + "/test.jpg";
File file = new File(path);
uploadManager.put(file, null, upToken,
new UpCompletionHandler() {
@Override
public void complete(String key, ResponseInfo respInfo,
JSONObject jsonData) {
if (respInfo.isOK()) {
Toast.makeText(MainActivity.this, "上傳成功!", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "上傳失敗!", Toast.LENGTH_SHORT).show();
Log.d(TAG, "error: " + respInfo.toString());
}
}
}, null);
}
}).start();
如果你這就急急忙忙準(zhǔn)備運(yùn)行,而又不打印任何日志,你會(huì)發(fā)現(xiàn)總是上傳失敗,并且還找不到原因。一開(kāi)始我也是一臉茫然,后來(lái)把info的信息打印出來(lái)之后看到access denied,立馬就知道了忘記設(shè)置權(quán)限了。
這里一定不要忘記給app加上讀取sd卡和聯(lián)網(wǎng)的權(quán)限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
加了權(quán)限這下該沒(méi)問(wèn)題了吧,說(shuō)不準(zhǔn),之前我們說(shuō)最好是用7.3.x版本,因?yàn)槲以谟?.0.x版本的時(shí)候遇到一個(gè)錯(cuò)誤
error:incorrect region, please use up-z2.qiniu.com
這個(gè)錯(cuò)誤跟地區(qū)有關(guān),在華南地區(qū)需要用up-z2.qiniu.com這個(gè)域名去訪問(wèn)。在文檔中之前被我忽略的一部分派上用場(chǎng)了

可以在創(chuàng)建UploadManager時(shí)給它傳入一些設(shè)置。
Configuration config = new Configuration.Builder()
.zone(Zone.zone2) // 設(shè)置區(qū)域,指定不同區(qū)域的上傳域名、備用域名、備用IP。
.build();
UploadManager uploadManager = new UploadManager(config);
Configuration config = new Configuration.Builder()
.zone(Zone.httpAutoZone) // 自動(dòng)識(shí)別
.build();
UploadManager uploadManager = new UploadManager(config);
點(diǎn)開(kāi)Zone我們就知道為什么設(shè)置這個(gè)能解決問(wèn)題了,出現(xiàn)的問(wèn)題就是讓我們使用up-z2.qiniu.com,而這個(gè)域名就在Zone.zone2里。
public abstract class Zone {
/**
* 華東機(jī)房, http
*/
public static final Zone zone0 =
createZone("upload.qiniu.com", "up.qiniu.com", "183.136.139.10", "115.231.182.136");
/**
* 華北機(jī)房, http
*/
public static final Zone zone1 =
createZone("upload-z1.qiniu.com", "up-z1.qiniu.com", "106.38.227.27", "106.38.227.28");
/**
* 華南機(jī)房, http
*/
public static final Zone zone2 =
createZone("upload-z2.qiniu.com", "up-z2.qiniu.com", "183.60.214.197", "14.152.37.7");
/**
* 北美機(jī)房, http
*/
public static final Zone zoneNa0 =
createZone("upload-na0.qiniu.com", "up-na0.qiniu.com", "23.236.102.3", "23.236.102.2");
/**
* 自動(dòng)判斷機(jī)房, http
*/
public static final AutoZone httpAutoZone = new AutoZone(false, null);
/**
* 自動(dòng)判斷機(jī)房, https
*/
public static final AutoZone httpsAutoZone = new AutoZone(true, null);
...
}
結(jié)果
上傳成功后,在test-demo存儲(chǔ)空間中就會(huì)增加一張新的圖片了

獲取圖片的外鏈地址
打開(kāi)七牛云端,test-demo內(nèi)容管理可以查看已上傳的文件,點(diǎn)擊可以查看外鏈地址:

這個(gè)外鏈地址可以通過(guò)domain+key的組合來(lái)得到,domain也能在內(nèi)容管理查看到:

key就是上傳后在七牛存儲(chǔ)上的文件名。
所以組合后的地址就是http://oq543v9g0.bkt.clouddn.com/lt0EG7Sm0nVKu3YaZAhE9XRoKgBr
token的加密算法
關(guān)于token是怎么生成的,可以看看官方文檔:
這里只提供加密的代碼,可以對(duì)照著官方文檔看看是如何加密的:
//七牛后臺(tái)的key
private static String AccessKey = "AccessKey";
//七牛后臺(tái)的secret
private static String SecretKey = "SecretKey";
private static final String MAC_NAME = "HmacSHA1";
private static final String ENCODING = "UTF-8";
public String getToken(){
JSONObject json = new JSONObject();
long deadline = System.currentTimeMillis() / 1000 + 3600;
try {
json.put("deadline", deadline);// 有效時(shí)間為一個(gè)小時(shí)
json.put("scope", bucketName);//存儲(chǔ)空間的名字
} catch (JSONException e) {
e.printStackTrace();
}
String encodedPutPolicy = UrlSafeBase64.encodeToString(json
.toString().getBytes());
byte[] sign = new byte[0];
try {
sign = HmacSHA1Encrypt(encodedPutPolicy, SecretKey);
} catch (Exception e) {
e.printStackTrace();
}
String encodedSign = UrlSafeBase64.encodeToString(sign);
String uploadToken = AccessKey + ':' + encodedSign + ':'
+ encodedPutPolicy;
return uploadToken;
}
public static byte[] HmacSHA1Encrypt(String encryptText, String encryptKey)
throws Exception {
byte[] data = encryptKey.getBytes(ENCODING);
// 根據(jù)給定的字節(jié)數(shù)組構(gòu)造一個(gè)密鑰,第二參數(shù)指定一個(gè)密鑰算法的名稱(chēng)
SecretKeySpec secretKey = new SecretKeySpec(data, MAC_NAME);
// 生成一個(gè)指定 Mac 算法 的 Mac 對(duì)象
Mac mac = Mac.getInstance(MAC_NAME);
// 用給定密鑰初始化 Mac 對(duì)象
mac.init(secretKey);
byte[] text = encryptText.getBytes(ENCODING);
// 完成 Mac 操作
return mac.doFinal(text);
}
進(jìn)階用法:多圖上傳
上述代碼只能完成一張圖片的上傳,而做項(xiàng)目的時(shí)候經(jīng)常需要一次性上傳多張圖片,這里提供兩種實(shí)現(xiàn)方法。
定義的全局變量:
private String upToken = "你的token";
private Configuration config = new Configuration.Builder()
.zone(Zone.zone2)
.build();
private UploadManager uploadManager = new UploadManager(config);
private int[] i = {0};//循環(huán)變量,表示現(xiàn)在正在上傳第幾張圖片
循環(huán)實(shí)現(xiàn)
優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單
缺點(diǎn):順序不好控制
new Thread(new Runnable() {
@Override
public void run() {
//兩張圖片路徑
String path1 = Environment.getExternalStorageDirectory() + "/test.jpg";
String path2 = Environment.getExternalStorageDirectory() + "/test-1.jpg";
final List<String> list = new ArrayList<>();
list.add(path1);
list.add(path2);
for(i[0]=0;i[0]<list.size();i[0]++) {
String file = list.get(i[0]);
uploadManager.put(file, null, upToken,
new UpCompletionHandler() {
@Override
public void complete(String key, ResponseInfo respInfo,
JSONObject jsonData) {
if (respInfo.isOK()) {
print("第" + i[0] +"張上傳成功!");
} else {
print("第" + i[0] +"張上傳失??!");
Log.d(TAG, "error: " + respInfo.error);
}
}
}, null);
}
}
}).start();
運(yùn)行結(jié)果:

說(shuō)明:
問(wèn)題出來(lái)了,為什么打印了兩個(gè)“第2張上傳成功”?原因是因?yàn)槭茄h(huán)進(jìn)行網(wǎng)絡(luò)請(qǐng)求,效果如下圖:

網(wǎng)絡(luò)請(qǐng)求是需要消耗時(shí)間的,而循環(huán)在開(kāi)啟一個(gè)網(wǎng)絡(luò)請(qǐng)求upload1后就立馬進(jìn)入下一個(gè)循環(huán)了(而不會(huì)等待網(wǎng)絡(luò)請(qǐng)求返回結(jié)果)。這時(shí),循環(huán)變量已經(jīng)由0變?yōu)?,但upload1可能還沒(méi)有返回結(jié)果,這時(shí)開(kāi)啟第二個(gè)網(wǎng)絡(luò)請(qǐng)求upload2,所以等兩個(gè)請(qǐng)求都完成時(shí),循環(huán)變量已經(jīng)變?yōu)?,因而兩個(gè)請(qǐng)求返回結(jié)果時(shí)都會(huì)打印“第2張上傳成功”。
遞歸實(shí)現(xiàn)
優(yōu)點(diǎn):順序清晰
缺點(diǎn):代碼多,復(fù)雜
private void click() {
//兩張圖片路徑
String path1 = Environment.getExternalStorageDirectory() + "/test.jpg";
String path2 = Environment.getExternalStorageDirectory() + "/test-1.jpg";
final List<String> list = new ArrayList<>();
list.add(path1);
list.add(path2);
//遞歸上傳兩張圖片
uploadMutliFiles(list, new UploadMutliListener() {
@Override
public void onUploadMutliSuccess() {
print(list.size() + "張圖片上傳成功!");
}
@Override
public void onUploadMutliFail(Error error) {
print("上傳失??!");
}
});
}
//上傳多張圖片
public void uploadMutliFiles(final List<String> filesUrls, final UploadMutliListener uploadMutliListener) {
if (filesUrls != null && filesUrls.size() > 0) {
final String url = filesUrls.get(i[0]);
uploadFile(url, new UploadListener() {
@Override
public void onUploadSuccess() {
final UploadListener uploadListener = this;
Log.d(TAG, "第" + (i[0]+1) + "張:" + url + "\t上傳成功!");
i[0]++;
//遞歸邊界條件
if (i[0] < filesUrls.size()) {
//七牛后臺(tái)對(duì)上傳的文件名是以時(shí)間戳來(lái)命名,以秒為單位,如果文件上傳過(guò)快,兩張圖片就會(huì)重名而上傳失敗,所以間隔1秒,保證上傳成功(具體會(huì)不會(huì)失敗呢?自己試一下看看)
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
uploadFile(filesUrls.get(i[0]), uploadListener);
}
}, 1000);
} else {
uploadMutliListener.onUploadMutliSuccess();
}
}
@Override
public void onUploadFail(Error error) {
print("第" + (i[0]+1) + "張上傳失敗!" + filesUrls.get(i[0]));
uploadMutliListener.onUploadMutliFail(error);
}
});
}
}
//上傳單個(gè)文件
public void uploadFile(final String filePath, final UploadListener uploadListener) {
if (filePath == null) return;
new Thread(new Runnable() {
@Override
public void run() {
if (uploadManager == null) {
uploadManager = new UploadManager();
}
uploadManager.put(filePath, null, upToken,
new UpCompletionHandler() {
@Override
public void complete(String key, ResponseInfo respInfo,
JSONObject jsonData) {
if (respInfo.isOK()) {
print(jsonData.toString());
uploadListener.onUploadSuccess();
} else {
uploadListener.onUploadFail(new Error("上傳失敗" + respInfo.error));
}
}
}, null);
}
}).start();
}
//上傳回調(diào)
public interface UploadListener {
void onUploadSuccess();
void onUploadFail(Error error);
}
//上傳多張文件回調(diào)
public interface UploadMutliListener {
void onUploadMutliSuccess();
void onUploadMutliFail(Error error);
}
運(yùn)行結(jié)果:

說(shuō)明:
看代碼規(guī)模就能明顯感覺(jué)兩者的不同,遞歸上傳代碼量相比循環(huán)多了很多,不過(guò)它的優(yōu)點(diǎn)就是它的順序十分清晰,遞歸調(diào)用的邏輯圖可以描繪成以下的效果:

為每個(gè)網(wǎng)絡(luò)請(qǐng)求upload設(shè)置一個(gè)監(jiān)聽(tīng)器,只有當(dāng)upload1的請(qǐng)求成功返回結(jié)果,也就是第一張圖片上傳成功后,才開(kāi)啟第2個(gè)上傳圖片的請(qǐng)求upload2,如果有upload3同樣接在upload2的success后,這樣形成了鏈?zhǔn)浇Y(jié)構(gòu),能確保圖片一定是按照順序上傳的。
總結(jié)
七牛云不局限于上傳圖片,其實(shí)任何文件都可以,所以如果是上傳視頻的話,道理都是一樣的。
七牛上傳文件的過(guò)程并不是很難,但是在網(wǎng)上找了一圈沒(méi)有找到合適的demo,所以在寫(xiě)完了之后立馬記了下來(lái)以備以后不時(shí)之需。