Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> android圖片壓縮上傳系列-service篇

android圖片壓縮上傳系列-service篇

編輯:關於Android編程

本篇文章是繼續上篇文章的續篇。主要目的是:通過Service來執行圖片壓縮任務來討論如何使用Service,如何處理任務量大的並發問題。

了解下Service

大家都知道如果有費時任務,這時需要將任務放到後台線程中執行,如果對操作的結果需要通過ui展示還需要在任務完成後通知前台更新。當然對於這種情況,大家也可以在Activity中啟動線程,在線程中通過Handler和sendMessage來通知Activity並執行更新ui的操作,但是更好的方法是將這些操作放到單獨的Service中。由於Activity生命周期的復雜性會導致管理線程的復雜度過高,而Service的生命周期相比Activity來說就只有創建和銷毀,更有利於執行管理耗時操作。

何時使用Service
android文檔官方解釋:Service表示不在影響用戶操作的情況下執行耗時的操作或提供供其它應用使用的功能。 Service類型
用來執行和用戶輸入無關的操作,比如音樂播放器,用戶退出應用的情況下還能執行播放操作 由用戶觸發的操作,如上傳圖片(在後台執行上傳,完畢後停止Service) Service生命周期
簡單講只有兩個必定被調用的回調函數,分別是onCreate(初始化),和onDestroy(清理) 啟動Service
可以通過兩種方式啟動:Context.startService()Context.bindService()
Context.startService()
Context.startService()啟動Service時,Service的onStartCommand()方法會被調用,並且在Service沒有銷毀前,不管前台執行多少次startService()操作,Service的onCreate只執行一遍,而onStartCommand()方法將被執行多遍。大家可以做個簡單測試如下:
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;
    }
}

在前台activity中執行:

//多次執行startService
for(int i = 1; i <= 10; ++i){
    Intent intent = new Intent(this,LGImgCompressorService.class);
    startService(intent);
}

測試時主要觀察打印日志,並查看線程ID,可以得出結論:
onCreate,onDestroy,onStartCommand都是在ui線程(主線程)中執行,onStartCommand執行了10遍,但onCreate和onDestroy只執行了一遍
其中onStartCommand方法最為復雜,Intent intent, int flags, int startId三個參數分別表示的含義大致如下:
1. intent
接收前台啟動sercie時傳入的intent,主要作用於前台需要給Service傳入相關參數
2. flags
標志位,標示本次啟動請求,可能的值有0,START_FLAG_REDELIVERY, START_FLAG_RETRY
3. startId
如果多次調用了onStartCommand,如果需要安全的停止Service,這個參數將會很有用

由於Service可能被意外(內存不足)終止,那麼系統該如何來處理這個Service呢?這時onStartCommand的返回值就起到作用了:
START_NOT_STICKY:Service被終止後不需要重新啟動,這對執行一次性的後台操作來說再合適不過了

START_STICKY:Service被終止後需要重新啟動,但是傳給onStartCommand的intent將為null

START_REDELIVER_INTENT:Service被終止後需要重新啟動,這時onStartCommand的intent將為Service銷毀之前最後一個intent

2.Context.bindService()

通過bindService來啟動的Service會一直運行,直到所有綁定的客戶端都斷開(unbindService)才會停止。注意這裡指的客戶端是指執行綁定Service操作所在的類實例(比如前面的activity,因為activity中執行了startService操作,這時我們稱這個activity為客戶端),本文章主要使用了第一種啟動方式,通過bindService啟動方式將放到後續文章重點討論,還望大家繼續關注。

銷毀Service
通過startService啟動的服務只能通過Service.stopSelf()或者Context.stopService來停止Service。

和其它組件(比如Activity)的交互
分為兩種情況:

如果在前台能持有Service對象,則可以通過BroadCast(廣播)以及callback回調的方式進行交互 如果在前台不能持有Service對象,則只能通過BraodCast或者AIDL的方式來進行交互
如果是在同一進程中也可以考慮使用EventBus。
通過廣播的方式非常簡單,只需要在適當的位置調用sendBroadCast()。比如:
public void uploadPicture(Bitmap bitmap){
    ...上傳
    sendBroadCast(new Intent(COMPLETE));
}

不管通過哪種方式,需要注意的是廣播的方式不適合Service和其它組件之間進行大規模的更新操作,比如更新進度條,如果有這方面的需求還是需要通過bindService的方式來綁定服務,因為這樣可以持有Service對象,然後可以通過callback的方式進行回調操作。演示代碼如下:
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)
        }
    }
    //執行後台任務
    public void onClick(View view){
        if(service != null){
            service.doTask();
        }
    }
    //更新進度ui
    @override
    public void onProgressing(){
        ...
    }
    //綁定成功回調此方法,初始化service成員(調用getService實際就是返回了LocalService實例)
    @override
    public void onServiceConnected(ComponentName name,IBinder iBinder){
        service = ((LocalService.LocalBinder) iBinder).getService();
        service.setCallBack(this);
    }
    //當Service斷開後回調
    @override
    public void onServiceDisconnected(ComponentName name){
        service = null;
    }
}

至於AIDL跨進程交互不在此討論了,這完全可以單獨用個專題來討論的。

最後回到文章主題,現在需要將壓縮任務放到Service中處理,應該考慮的問題是:
1. 用單線程多任務的方式處理,解決方案如下:
把所有需要壓縮的任務放到一個任務隊列中,開啟後台線程挨個處理隊列中的任務,處理完一個移除一個。其實還是很簡單的,那麼需要我們自己來維護這個線程和任務隊列嗎?其實android給我們提供了IntentService來專門處理這種情況,其核心思想是在後台線程生成一個Looper,在Looper中dispatchMessage獲取消息隊列中的消息,在IntentService中創建Handler來發送和處理消息。使用IntentService還有個好處就是不需要我們在手動結束Service。
2. 用多線程多任務的方式處理,解決方案如下:
這種方式就是啟動多個線程並記錄本次任務總數量,每個線程單獨執行一個壓縮任務,執行完一個任務數量減1,如果最後任務數為0,則停止Service並執行清理操作。由於涉及在一個Service中啟動多個線程,所以必然需要處理所謂的“共享資源的問題”

最後使用代碼演示以上兩種方案的處理:

用單線程多任務的方式處理,由於只是單線程所以不需要考慮“共享資源的問題”,代碼相對簡單清晰
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 compressResults = new ArrayList<>();//存儲壓縮任務的返回結果

    public LGImgCompressorIntentService() {
        super("LGImgCompressorIntentService");
        setIntentRedelivery(false);//避免出異常後service重新啟動
    }

    @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);//發送壓縮結束廣播
        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)) {
                //取出從前台通過intent傳入的壓縮參數
                final CompressServiceParam param1 = intent.getParcelableExtra(Constanse.COMPRESS_PARAM);
                handleActionCompress(param1);
            }
        }
    }
    //執行壓縮操作
    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兩個類,分別用於處理壓縮的返回結果和傳給Service用的壓縮參數
代碼如下(由於篇幅問題省咧了很多代碼,如果需要請轉到我的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();
    }
    ...
}

由於通過intent.putXXX()方法要將CompressServiceParam實例put到Intent那麼CompressServiceParam必須實現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中啟動服務,核心代碼如下

ArrayList 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);
}
//廣播接收類
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 compressResults =
                    (ArrayList)intent.getSerializableExtra(Constanse.KEY_COMPRESS_RESULT);
        }
    }
}
@Override
protected void onCreate(Bundle savedInstanceState) {
    //注冊廣播
    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);//取消注冊
    }
}
用多線程多任務的方式處理,大部分代碼和單線程類似,只是需要將任務放到線程池中處理並處理好數據安全問題。核心代碼如下:
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;//記錄任務數量
    private ExecutorService executorService;
    private final Object lock = new Object();//對象鎖
    private void doCompressImages(final Intent intent,final int taskId){
        final ArrayList paramArrayList = intent.getParcelableArrayListExtra(Constanse.COMPRESS_PARAM);
        synchronized (lock){
            taskNumber += paramArrayList.size();
        }
        //如果paramArrayList過大,為了避免"The application may be doing too much work on its main thread"的問題,將任務的創建和執行統一放在後台線程中執行
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < paramArrayList.size(); ++i){
                    executorService.execute(new CompressTask(paramArrayList.get(i),taskId));//將任務放入線程池中執行
                }
            }
        }).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() {
            ...
            //加鎖,避免並發修改數據導致髒數據的情況
            synchronized (lock){
                compressResults.add(compressResult);
                taskNumber--;
                if(taskNumber <= 0){
                    stopSelf(taskId);//通過onStartCommand中的startId來正確的關閉Serivce
                }
            }
        }
    }
    @Override
    public IBinder onBind(Intent intent) {
        throw null;
    }
}

兩個方案的對比

我們主要通過內存的使用量和壓縮所耗費的時間來對比下以上兩種方案,我的測試用手機對91個圖片進行壓縮處理後的結果:
方案1:
耗時8211ms
內存圖:
Paste_Image.png
方案2:
耗時1872ms
Paste_Image.png
可以看出方案1的內存消耗比較平穩但是耗時大,而方案2的內存消耗大,內存峰值接近100M。其實發生對於這種情況也是可以理解的,方案1是單線程的一次只處理一個壓縮任務,而方案2是多線程並發的,假設瞬間並發處理90個任務每個任務消耗1M內存,那麼在這瞬間將消耗90M內存,再加上線程的創建和消耗所消耗的內存肯定就在90M以上了。
至於哪種方案更好,這需要看實際業務了,這是典型的“用時間換空間”還是用“空間換時間”的問題了。
對於方案2還是可以進行一定的優化的,在Service的onCreate中,我們用了executorService = Executors.newCachedThreadPool();來生成線程池,其底層代碼為:
Executors.java
java
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}


ThreadPoolExecutor前三個參數分別表示:
corePoolSize(核心池大小)
maximumPoolSize(最大線程數量)
keepAliveTime(當池中線程數量大於corePoolSize時,線程等待新任務的的最大時間。比如現在線程池有兩個A,B線程,A線程執行完了任務處於等待新任務的狀態,如果新任務在keepAliveTime時間內還沒有加入進來,那麼A線程將被銷毀)。
newCachedThreadPool的默認實現是:核心池大小為0,最大線程數量為MAX_VALUE,保持時間為60秒,也就是說如果方案2中有91個並行處理的任務,那麼將生成91個線程,這個數量還是非常大的。
換種方式考慮問題,能不能要線程池只保留有限的線程數,如果任務數超出了線程數則加入等待隊列中,等有空閒的線程時再用這個空閒的線程處理任務?這樣我們即保證了一定的並發數提高了處理速度,同時不會瞬間占用過多的內存開銷。可以通過Executors.newFixedThreadPool(size)來達到上面的目的,將方案2中onCreate,創建線程池的代碼改為:
Executors.newFixedThreadPool(10)得到的測試結果如下:
耗時1712ms
Paste_Image.png

寫在最後

以上方案並不存在絕對的哪個好,哪個壞之分。如果處理的任務數量不多比如40個以下,建議大家使用方案1,具體的數量還需要多測試找到合適點。
如果確實有大量的任務需要處理則采用方案2,但是創建線程池用newFixedThreadPool方式來創建,另外可以考慮將Service以remote方式在另外的進程中執行,這樣其占用的內存將不會占用本app的內存,以remote方式運行只需在配置service的AndroidManifest.xml中以如下方式配置即可:
其中process的:表示其運行在獨立進程中。
最後我們也可以綜合采用方案1和方案2來處理,比如在啟動service之前先判斷當前任務的數量,如果小於一定的值則采用方案1,否則采用方案2這樣動態的采取不同的策略

本篇文章字數較多,感謝大家非常耐心的讀完~~希望本篇文章對大家有所幫助

demo開源github地址如下:
LGImageCompressor
 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved