本篇文章是繼續(xù)上篇android圖片壓縮上傳系列-基礎(chǔ)篇文章的續(xù)篇。主要目的是:通過(guò)Service來(lái)執(zhí)行圖片壓縮任務(wù)來(lái)討論如何使用Service,如何處理任務(wù)量大的并發(fā)問(wèn)題。
了解下Service
大家都知道如果有費(fèi)時(shí)任務(wù),這時(shí)需要將任務(wù)放到后臺(tái)線程中執(zhí)行,如果對(duì)操作的結(jié)果需要通過(guò)ui展示還需要在任務(wù)完成后通知前臺(tái)更新。當(dāng)然對(duì)于這種情況,大家也可以在Activity中啟動(dòng)線程,在線程中通過(guò)Handler和sendMessage來(lái)通知Activity并執(zhí)行更新ui的操作,但是更好的方法是將這些操作放到單獨(dú)的Service中。由于Activity生命周期的復(fù)雜性會(huì)導(dǎo)致管理線程的復(fù)雜度過(guò)高,而Service的生命周期相比Activity來(lái)說(shuō)就只有創(chuàng)建和銷(xiāo)毀,更有利于執(zhí)行管理耗時(shí)操作。
- 何時(shí)使用Service
android文檔官方解釋?zhuān)篠ervice表示不在影響用戶(hù)操作的情況下執(zhí)行耗時(shí)的操作或提供供其它應(yīng)用使用的功能。 - Service類(lèi)型
- 用來(lái)執(zhí)行和用戶(hù)輸入無(wú)關(guān)的操作,比如音樂(lè)播放器,用戶(hù)退出應(yīng)用的情況下還能執(zhí)行播放操作
- 由用戶(hù)觸發(fā)的操作,如上傳圖片(在后臺(tái)執(zhí)行上傳,完畢后停止Service)
- Service生命周期
簡(jiǎn)單講只有兩個(gè)必定被調(diào)用的回調(diào)函數(shù),分別是onCreate(初始化),和onDestroy(清理) - 啟動(dòng)Service
可以通過(guò)兩種方式啟動(dòng):Context.startService()和Context.bindService()- Context.startService()
Context.startService()啟動(dòng)Service時(shí),Service的onStartCommand()方法會(huì)被調(diào)用,并且在Service沒(méi)有銷(xiāo)毀前,不管前臺(tái)執(zhí)行多少次startService()操作,Service的onCreate只執(zhí)行一遍,而onStartCommand()方法將被執(zhí)行多遍。大家可以做個(gè)簡(jiǎn)單測(cè)試如下:
- Context.startService()
public class LGImgCompressorService extends Service {
private static final String TAG = "LGImgCompressorService";
public LGImgCompressorService() {
}
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG,"onCreate... thread id:" + Thread.currentThread().getId());
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG,"onDestroy...thread id:" + Thread.currentThread().getId());
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG,"onStartCommand thread id:" + Thread.currentThread().getId() + " startId:" + startId);
return Service.START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
throw null;
}
}
在前臺(tái)activity中執(zhí)行:
//多次執(zhí)行startService
for(int i = 1; i <= 10; ++i){
Intent intent = new Intent(this,LGImgCompressorService.class);
startService(intent);
}
測(cè)試時(shí)主要觀察打印日志,并查看線程ID,可以得出結(jié)論:
onCreate,onDestroy,onStartCommand都是在ui線程(主線程)中執(zhí)行,onStartCommand執(zhí)行了10遍,但onCreate和onDestroy只執(zhí)行了一遍
其中onStartCommand方法最為復(fù)雜,Intent intent, int flags, int startId三個(gè)參數(shù)分別表示的含義大致如下:
- intent
接收前臺(tái)啟動(dòng)sercie時(shí)傳入的intent,主要作用于前臺(tái)需要給Service傳入相關(guān)參數(shù) - flags
標(biāo)志位,標(biāo)示本次啟動(dòng)請(qǐng)求,可能的值有0,START_FLAG_REDELIVERY, START_FLAG_RETRY - startId
如果多次調(diào)用了onStartCommand,如果需要安全的停止Service,這個(gè)參數(shù)將會(huì)很有用
由于Service可能被意外(內(nèi)存不足)終止,那么系統(tǒng)該如何來(lái)處理這個(gè)Service呢?這時(shí)onStartCommand的返回值就起到作用了:
START_NOT_STICKY:Service被終止后不需要重新啟動(dòng),這對(duì)執(zhí)行一次性的后臺(tái)操作來(lái)說(shuō)再合適不過(guò)了
START_STICKY:Service被終止后需要重新啟動(dòng),但是傳給onStartCommand的intent將為null
START_REDELIVER_INTENT:Service被終止后需要重新啟動(dòng),這時(shí)onStartCommand的intent將為Service銷(xiāo)毀之前最后一個(gè)intent
- Context.bindService()
通過(guò)bindService來(lái)啟動(dòng)的Service會(huì)一直運(yùn)行,直到所有綁定的客戶(hù)端都斷開(kāi)(unbindService)才會(huì)停止。注意這里指的客戶(hù)端是指執(zhí)行綁定Service操作所在的類(lèi)實(shí)例(比如前面的activity,因?yàn)閍ctivity中執(zhí)行了startService操作,這時(shí)我們稱(chēng)這個(gè)activity為客戶(hù)端),本文章主要使用了第一種啟動(dòng)方式,通過(guò)bindService啟動(dòng)方式將放到后續(xù)文章重點(diǎn)討論,還望大家繼續(xù)關(guān)注。
銷(xiāo)毀Service
通過(guò)startService啟動(dòng)的服務(wù)只能通過(guò)Service.stopSelf()或者Context.stopService來(lái)停止Service。-
和其它組件(比如Activity)的交互
分為兩種情況:- 如果在前臺(tái)能持有Service對(duì)象,則可以通過(guò)BroadCast(廣播)以及callback回調(diào)的方式進(jìn)行交互
- 如果在前臺(tái)不能持有Service對(duì)象,則只能通過(guò)BraodCast或者AIDL的方式來(lái)進(jìn)行交互
如果是在同一進(jìn)程中也可以考慮使用EventBus。
通過(guò)廣播的方式非常簡(jiǎn)單,只需要在適當(dāng)?shù)奈恢谜{(diào)用sendBroadCast()。比如:
public void uploadPicture(Bitmap bitmap){
...上傳
sendBroadCast(new Intent(COMPLETE));
}
不管通過(guò)哪種方式,需要注意的是廣播的方式不適合Service和其它組件之間進(jìn)行大規(guī)模的更新操作,比如更新進(jìn)度條,如果有這方面的需求還是需要通過(guò)bindService的方式來(lái)綁定服務(wù),因?yàn)檫@樣可以持有Service對(duì)象,然后可以通過(guò)callback的方式進(jìn)行回調(diào)操作。演示代碼如下:
ServiceTest.java
public class LocalService extends Service{
private CallBack callback;
private LocalBinder localBinder = new LocalBinder();
public IBinder onBind(Intent intent){
return localBinder;
}
public void doTask(){
new MyTask().execute();
}
public void setCallback(CallBack callback){
this.callback = callback;
}
public class LocalBinder extends Binder(){
public LocalService getService(){
return LocalService.this;
}
}
private final class MyTask extends AsyncTask<>{
@override
protected void onPreExecute(){
...
}
@override
protected void onProgressUpdate(){
...
callback.onProgressing();
}
@override
protected void onPostExecute(){
...
callback.onCompleted();
}
}
}
MyActivity.java
public class MyActivity extends Activity implements CallBack{
...
LocalService service;
@override
protected void onResume(){
...
Intent intent = new Intent(this,LocalService.class);
bindService(intent,this,BIND_AUTO_CREATE);
}
@override
protected void onPause(){
...
if(service != null){
service.setCallBack(this);
unbindService(this)
}
}
//執(zhí)行后臺(tái)任務(wù)
public void onClick(View view){
if(service != null){
service.doTask();
}
}
//更新進(jìn)度ui
@override
public void onProgressing(){
...
}
//綁定成功回調(diào)此方法,初始化service成員(調(diào)用getService實(shí)際就是返回了LocalService實(shí)例)
@override
public void onServiceConnected(ComponentName name,IBinder iBinder){
service = ((LocalService.LocalBinder) iBinder).getService();
service.setCallBack(this);
}
//當(dāng)Service斷開(kāi)后回調(diào)
@override
public void onServiceDisconnected(ComponentName name){
service = null;
}
}
至于AIDL跨進(jìn)程交互不在此討論了,這完全可以單獨(dú)用個(gè)專(zhuān)題來(lái)討論的。
最后回到文章主題,現(xiàn)在需要將壓縮任務(wù)放到Service中處理,應(yīng)該考慮的問(wèn)題是:
- 用單線程多任務(wù)的方式處理,解決方案如下:
把所有需要壓縮的任務(wù)放到一個(gè)任務(wù)隊(duì)列中,開(kāi)啟后臺(tái)線程挨個(gè)處理隊(duì)列中的任務(wù),處理完一個(gè)移除一個(gè)。其實(shí)還是很簡(jiǎn)單的,那么需要我們自己來(lái)維護(hù)這個(gè)線程和任務(wù)隊(duì)列嗎?其實(shí)android給我們提供了IntentService來(lái)專(zhuān)門(mén)處理這種情況,其核心思想是在后臺(tái)線程生成一個(gè)Looper,在Looper中dispatchMessage獲取消息隊(duì)列中的消息,在IntentService中創(chuàng)建Handler來(lái)發(fā)送和處理消息。使用IntentService還有個(gè)好處就是不需要我們?cè)谑謩?dòng)結(jié)束Service。至于IntentService的內(nèi)部原理,大家可以參考我的文章從源碼分析IntentService,強(qiáng)烈建議閱讀一下 - 用多線程多任務(wù)的方式處理,解決方案如下:
這種方式就是啟動(dòng)多個(gè)線程并記錄本次任務(wù)總數(shù)量,每個(gè)線程單獨(dú)執(zhí)行一個(gè)壓縮任務(wù),執(zhí)行完一個(gè)任務(wù)數(shù)量減1,如果最后任務(wù)數(shù)為0,則停止Service并執(zhí)行清理操作。由于涉及在一個(gè)Service中啟動(dòng)多個(gè)線程,所以必然需要處理所謂的“共享資源的問(wèn)題”
最后使用代碼演示以上兩種方案的處理:
- 用單線程多任務(wù)的方式處理,由于只是單線程所以不需要考慮“共享資源的問(wèn)題”,代碼相對(duì)簡(jiǎn)單清晰
LGImgCompressorIntentService.java
public class LGImgCompressorIntentService extends IntentService {
private final String TAG = LGImgCompressorIntentService.class.getSimpleName();
private static final String ACTION_COMPRESS = "gui.com.lgimagecompressor.action.COMPRESS";
private ArrayList<LGImgCompressor.CompressResult> compressResults = new ArrayList<>();//存儲(chǔ)壓縮任務(wù)的返回結(jié)果
public LGImgCompressorIntentService() {
super("LGImgCompressorIntentService");
setIntentRedelivery(false);//避免出異常后service重新啟動(dòng)
}
@Override
public void onCreate() {
super.onCreate();
Intent intent = new Intent(Constanse.ACTION_COMPRESS_BROADCAST);
intent.putExtra(Constanse.KEY_COMPRESS_FLAG,Constanse.FLAG_BEGAIIN);
sendBroadcast(intent);
Log.d(TAG,"onCreate...");
}
@Override
public void onDestroy() {
super.onDestroy();
Intent intent = new Intent(Constanse.ACTION_COMPRESS_BROADCAST);
intent.putExtra(Constanse.KEY_COMPRESS_FLAG,Constanse.FLAG_END);
intent.putParcelableArrayListExtra(Constanse.KEY_COMPRESS_RESULT,compressResults);
sendBroadcast(intent);//發(fā)送壓縮結(jié)束廣播
compressResults.clear();
Log.d(TAG,"onDestroy...");
}
public static void startActionCompress(Context context, CompressServiceParam param) {
Intent intent = new Intent(context, LGImgCompressorIntentService.class);
intent.setAction(ACTION_COMPRESS);
intent.putExtra(Constanse.COMPRESS_PARAM, param);
context.startService(intent);
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
final String action = intent.getAction();
if (ACTION_COMPRESS.equals(action)) {
//取出從前臺(tái)通過(guò)intent傳入的壓縮參數(shù)
final CompressServiceParam param1 = intent.getParcelableExtra(Constanse.COMPRESS_PARAM);
handleActionCompress(param1);
}
}
}
//執(zhí)行壓縮操作
private void handleActionCompress(CompressServiceParam param) {
int outwidth = param.getOutWidth();
int outHieight = param.getOutHeight();
int maxFileSize = param.getMaxFileSize();
String srcImageUri = param.getSrcImageUri();
LGImgCompressor.CompressResult compressResult = new LGImgCompressor.CompressResult();
String outPutPath = null;
try {
outPutPath = LGImgCompressor.getInstance(this).compressImage(srcImageUri, outwidth, outHieight, maxFileSize);
} catch (Exception e) {
}
compressResult.setSrcPath(srcImageUri);
compressResult.setOutPath(outPutPath);
if (outPutPath == null) {
compressResult.setStatus(LGImgCompressor.CompressResult.RESULT_ERROR);
}
compressResults.add(compressResult);
}
}
相比上一篇文章的版本,此次新增了CompressResult和CompressServiceParam兩個(gè)類(lèi),分別用于處理壓縮的返回結(jié)果和傳給Service用的壓縮參數(shù)
代碼如下(由于篇幅問(wèn)題省咧了很多代碼,如果需要請(qǐng)轉(zhuǎn)到我的github地址):
public class CompressServiceParam implements Parcelable {
private int outWidth;
private int outHeight;
private int maxFileSize;
private String srcImageUri;
public CompressServiceParam() {
}
protected CompressServiceParam(Parcel in) {
outWidth = in.readInt();
outHeight = in.readInt();
maxFileSize = in.readInt();
srcImageUri = in.readString();
}
...
}
由于通過(guò)intent.putXXX()方法要將CompressServiceParam實(shí)例put到Intent那么CompressServiceParam必須實(shí)現(xiàn)Parcelable接口
CompressResult.java
public static class CompressResult implements Parcelable{
public static final int RESULT_OK = 0;//成功
public static final int RESULT_ERROR = 1;//失敗
private int status = RESULT_OK;//
private String srcPath;//原圖目錄
private String outPath;//輸出圖的目錄
public CompressResult(){
}
protected CompressResult(Parcel in) {
status = in.readInt();
srcPath = in.readString();
outPath = in.readString();
}
...
}
最后在ServiceCompressActivity.java中啟動(dòng)服務(wù),核心代碼如下
ArrayList<Uri> compressFiles = getImagesPathFormAlbum();//獲取所有圖片的uri地址
Log.d(TAG, compressFiles.size() + "compresse begain");
int size = compressFiles.size() > 10 ? 10:compressFiles.size();
for (int i = 0; i < compressFiles.size(); ++i) {
Uri uri = compressFiles.get(i);
CompressServiceParam param = new CompressServiceParam();
param.setOutHeight(800);
param.setOutWidth(600);
param.setMaxFileSize(400);
param.setSrcImageUri(uri.toString());
LGImgCompressorIntentService.startActionCompress(ServiceCompressActivity.this, param);
}
//廣播接收類(lèi)
private class CompressingReciver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "onReceive:" + Thread.currentThread().getId());
int flag = intent.getIntExtra(Constanse.KEY_COMPRESS_FLAG,-1);
Log.d(TAG," flag:" + flag);
if(flag == Constanse.FLAG_BEGAIIN){
return;
}
if(flag == Constanse.FLAG_END){
ArrayList<LGImgCompressor.CompressResult> compressResults =
(ArrayList<LGImgCompressor.CompressResult>)intent.getSerializableExtra(Constanse.KEY_COMPRESS_RESULT);
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
//注冊(cè)廣播
reciver = new CompressingReciver();
IntentFilter intentFilter = new IntentFilter(Constanse.ACTION_COMPRESS_BROADCAST);
registerReceiver(reciver, intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
if(reciver != null){
unregisterReceiver(reciver);//取消注冊(cè)
}
}
- 用多線程多任務(wù)的方式處理,大部分代碼和單線程類(lèi)似,只是需要將任務(wù)放到線程池中處理并處理好數(shù)據(jù)安全問(wèn)題。核心代碼如下:
LGImgCompressorService.java
public class LGImgCompressorService extends Service {
public LGImgCompressorService() {
}
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG,"onCreate...");
executorService = Executors.newCachedThreadPool();
// executorService = Executors.newFixedThreadPool(10);
}
@Override
public void onDestroy() {
super.onDestroy();
...
sendBroadcast(intent);
compressResults.clear();
executorService.shutdownNow();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
doCompressImages(intent,startId);
return Service.START_NOT_STICKY;
}
private int taskNumber;//記錄任務(wù)數(shù)量
private ExecutorService executorService;
private final Object lock = new Object();//對(duì)象鎖
private void doCompressImages(final Intent intent,final int taskId){
final ArrayList<CompressServiceParam> paramArrayList = intent.getParcelableArrayListExtra(Constanse.COMPRESS_PARAM);
synchronized (lock){
taskNumber += paramArrayList.size();
}
//如果paramArrayList過(guò)大,為了避免"The application may be doing too much work on its main thread"的問(wèn)題,將任務(wù)的創(chuàng)建和執(zhí)行統(tǒng)一放在后臺(tái)線程中執(zhí)行
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < paramArrayList.size(); ++i){
executorService.execute(new CompressTask(paramArrayList.get(i),taskId));//將任務(wù)放入線程池中執(zhí)行
}
}
}).start();
}
private class CompressTask implements Runnable{
private CompressServiceParam param;
private int taskId ;
private CompressTask(CompressServiceParam compressServiceParam,int taskId){
this.param = compressServiceParam;
this.taskId = taskId;
}
@Override
public void run() {
...
//加鎖,避免并發(fā)修改數(shù)據(jù)導(dǎo)致臟數(shù)據(jù)的情況
synchronized (lock){
compressResults.add(compressResult);
taskNumber--;
if(taskNumber <= 0){
stopSelf(taskId);//通過(guò)onStartCommand中的startId來(lái)正確的關(guān)閉Serivce
}
}
}
}
@Override
public IBinder onBind(Intent intent) {
throw null;
}
}
兩個(gè)方案的對(duì)比
我們主要通過(guò)內(nèi)存的使用量和壓縮所耗費(fèi)的時(shí)間來(lái)對(duì)比下以上兩種方案,我的測(cè)試用手機(jī)對(duì)91個(gè)圖片進(jìn)行壓縮處理后的結(jié)果:
方案1:
耗時(shí)8211ms
內(nèi)存圖:

方案2:
耗時(shí)1872ms

可以看出方案1的內(nèi)存消耗比較平穩(wěn)但是耗時(shí)大,而方案2的內(nèi)存消耗大,內(nèi)存峰值接近100M。其實(shí)發(fā)生對(duì)于這種情況也是可以理解的,方案1是單線程的一次只處理一個(gè)壓縮任務(wù),而方案2是多線程并發(fā)的,假設(shè)瞬間并發(fā)處理90個(gè)任務(wù)每個(gè)任務(wù)消耗1M內(nèi)存,那么在這瞬間將消耗90M內(nèi)存,再加上線程的創(chuàng)建和消耗所消耗的內(nèi)存肯定就在90M以上了。
至于哪種方案更好,這需要看實(shí)際業(yè)務(wù)了,這是典型的“用時(shí)間換空間”還是用“空間換時(shí)間”的問(wèn)題了。
對(duì)于方案2還是可以進(jìn)行一定的優(yōu)化的,在Service的onCreate中,我們用了
executorService = Executors.newCachedThreadPool();來(lái)生成線程池,其底層代碼為:Executors.java
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
ThreadPoolExecutor前三個(gè)參數(shù)分別表示:
corePoolSize(核心池大?。?br>
maximumPoolSize(最大線程數(shù)量)
keepAliveTime(當(dāng)池中線程數(shù)量大于corePoolSize時(shí),線程等待新任務(wù)的的最大時(shí)間。比如現(xiàn)在線程池有兩個(gè)A,B線程,A線程執(zhí)行完了任務(wù)處于等待新任務(wù)的狀態(tài),如果新任務(wù)在keepAliveTime時(shí)間內(nèi)還沒(méi)有加入進(jìn)來(lái),那么A線程將被銷(xiāo)毀)。
newCachedThreadPool的默認(rèn)實(shí)現(xiàn)是:核心池大小為0,最大線程數(shù)量為MAX_VALUE,保持時(shí)間為60秒,也就是說(shuō)如果方案2中有91個(gè)并行處理的任務(wù),那么將生成91個(gè)線程,這個(gè)數(shù)量還是非常大的。
換種方式考慮問(wèn)題,能不能要線程池只保留有限的線程數(shù),如果任務(wù)數(shù)超出了線程數(shù)則加入等待隊(duì)列中,等有空閑的線程時(shí)再用這個(gè)空閑的線程處理任務(wù)?這樣我們即保證了一定的并發(fā)數(shù)提高了處理速度,同時(shí)不會(huì)瞬間占用過(guò)多的內(nèi)存開(kāi)銷(xiāo)??梢酝ㄟ^(guò)Executors.newFixedThreadPool(size)來(lái)達(dá)到上面的目的,將方案2中onCreate,創(chuàng)建線程池的代碼改為:
Executors.newFixedThreadPool(10)得到的測(cè)試結(jié)果如下:
耗時(shí)1712ms

寫(xiě)在最后
以上方案并不存在絕對(duì)的哪個(gè)好,哪個(gè)壞之分。如果處理的任務(wù)數(shù)量不多比如40個(gè)以下,建議大家使用方案1,具體的數(shù)量還需要多測(cè)試找到合適點(diǎn)。
如果確實(shí)有大量的任務(wù)需要處理則采用方案2,但是創(chuàng)建線程池用newFixedThreadPool方式來(lái)創(chuàng)建,另外可以考慮將Service以remote方式在另外的進(jìn)程中執(zhí)行,這樣其占用的內(nèi)存將不會(huì)占用本app的內(nèi)存,以remote方式運(yùn)行只需在配置service的AndroidManifest.xml中以如下方式配置即可:
<service android:name=".LGImgCompressorService" android:process=":lg_remote"/>其中process的:表示其運(yùn)行在獨(dú)立進(jìn)程中。
最后我們也可以綜合采用方案1和方案2來(lái)處理,比如在啟動(dòng)service之前先判斷當(dāng)前任務(wù)的數(shù)量,如果小于一定的值則采用方案1,否則采用方案2這樣動(dòng)態(tài)的采取不同的策略
本篇文章字?jǐn)?shù)較多,感謝大家非常耐心的讀完~~希望本篇文章對(duì)大家有所幫助
demo開(kāi)源github地址如下:
LGImageCompressor
歡迎大家訪問(wèn)并star,如果有任何問(wèn)題可以在評(píng)論中加以提問(wèn),謝謝~~