android图片压缩上传系列-service篇

本篇文章是继续上篇android图片压缩上传系列-基础篇文章的续篇。主要目的是:通过Service来执行图片压缩任务来讨论如何使用Service,如何处理任务量大的并发问题。

了解下Service

大家都知道如果有费时任务,这时需要将任务放到后台线程中执行,如果对操作的结果需要通过ui展示还需要在任务完成后通知前台更新。当然对于这种情况,大家也可以在Activity中启动线程,在线程中通过Handler和sendMessage来通知Activity并执行更新ui的操作,但是更好的方法是将这些操作放到单独的Service中。由于Activity生命周期的复杂性会导致管理线程的复杂度过高,而Service的生命周期相比Activity来说就只有创建和销毁,更有利于执行管理耗时操作。

  • 何时使用Service
    android文档官方解释:Service表示不在影响用户操作的情况下执行耗时的操作或提供供其它应用使用的功能。
  • Service类型
  1. 用来执行和用户输入无关的操作,比如音乐播放器,用户退出应用的情况下还能执行播放操作
  2. 由用户触发的操作,如上传图片(在后台执行上传,完毕后停止Service)
  • Service生命周期
    简单讲只有两个必定被调用的回调函数,分别是onCreate(初始化),和onDestroy(清理)
  • 启动Service
    可以通过两种方式启动:Context.startService()Context.bindService()
    1. 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

  1. Context.bindService()
    通过bindService来启动的Service会一直运行,直到所有绑定的客户端都断开(unbindService)才会停止。注意这里指的客户端是指执行绑定Service操作所在的类实例(比如前面的activity,因为activity中执行了startService操作,这时我们称这个activity为客户端),本文章主要使用了第一种启动方式,通过bindService启动方式将放到后续文章重点讨论,还望大家继续关注。
  • 销毁Service
    通过startService启动的服务只能通过Service.stopSelf()或者Context.stopService来停止Service。

  • 和其它组件(比如Activity)的交互
    分为两种情况:

    1. 如果在前台能持有Service对象,则可以通过BroadCast(广播)以及callback回调的方式进行交互
    2. 如果在前台不能持有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。至于IntentService的内部原理,大家可以参考我的文章从源码分析IntentService,强烈建议阅读一下
  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<LGImgCompressor.CompressResult> 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<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);
}
//广播接收类
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) {
    //注册广播
    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<CompressServiceParam> 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

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                              60L, TimeUnit.SECONDS,
                              new SynchronousQueue<Runnable>());
}

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中以如下方式配置即可:
<service android:name=".LGImgCompressorService" android:process=":lg_remote"/>其中process的:表示其运行在独立进程中。
最后我们也可以综合采用方案1和方案2来处理,比如在启动service之前先判断当前任务的数量,如果小于一定的值则采用方案1,否则采用方案2这样动态的采取不同的策略

本篇文章字数较多,感谢大家非常耐心的读完~~希望本篇文章对大家有所帮助

demo开源github地址如下:
LGImageCompressor
欢迎大家访问并star,如果有任何问题可以在评论中加以提问,谢谢~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 参考: 服务|Android Developers 一. 什么是服务 服务是一个可以在后台执行长时间运行操作而不提...
    NickelFox阅读 540评论 0 3
  • 上篇我们讲解了Android中的5中等级的进程,分别是:前台进程、可见进程、服务进程、后台进程、空进程。系统会按照...
    徐爱卿阅读 3,838评论 6 33
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 前言:本文所写的是博主的个人见解,如有错误或者不恰当之处,欢迎私信博主,加以改正!原文链接,demo链接 Serv...
    PassersHowe阅读 1,400评论 0 5
  • 溪水不因山川的阻挡而止步不前,而是适应地势终究涌入大海。 向日葵不因太阳的东升西落而暗自哭泣,而是跟着太阳转终究结...
    独钓寒江雪iris阅读 353评论 0 1