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

本篇文章是繼續(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)型
  1. 用來(lái)執(zhí)行和用戶(hù)輸入無(wú)關(guān)的操作,比如音樂(lè)播放器,用戶(hù)退出應(yīng)用的情況下還能執(zhí)行播放操作
  2. 由用戶(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()
    1. 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è)試如下:
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ù)分別表示的含義大致如下:

  1. intent
    接收前臺(tái)啟動(dòng)sercie時(shí)傳入的intent,主要作用于前臺(tái)需要給Service傳入相關(guān)參數(shù)
  2. flags
    標(biāo)志位,標(biāo)示本次啟動(dòng)請(qǐng)求,可能的值有0,START_FLAG_REDELIVERY, START_FLAG_RETRY
  3. 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

  1. 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)的交互
    分為兩種情況:

    1. 如果在前臺(tái)能持有Service對(duì)象,則可以通過(guò)BroadCast(廣播)以及callback回調(diào)的方式進(jìn)行交互
    2. 如果在前臺(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)題是:

  1. 用單線程多任務(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)烈建議閱讀一下
  2. 用多線程多任務(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)存圖:

Paste_Image.png

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

可以看出方案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

Paste_Image.png

寫(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),謝謝~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 參考: 服務(wù)|Android Developers 一. 什么是服務(wù) 服務(wù)是一個(gè)可以在后臺(tái)執(zhí)行長(zhǎng)時(shí)間運(yùn)行操作而不提...
    NickelFox閱讀 601評(píng)論 0 3
  • 上篇我們講解了Android中的5中等級(jí)的進(jìn)程,分別是:前臺(tái)進(jìn)程、可見(jiàn)進(jìn)程、服務(wù)進(jìn)程、后臺(tái)進(jìn)程、空進(jìn)程。系統(tǒng)會(huì)按照...
    徐愛(ài)卿閱讀 3,983評(píng)論 6 33
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,564評(píng)論 19 139
  • 前言:本文所寫(xiě)的是博主的個(gè)人見(jiàn)解,如有錯(cuò)誤或者不恰當(dāng)之處,歡迎私信博主,加以改正!原文鏈接,demo鏈接 Serv...
    PassersHowe閱讀 1,516評(píng)論 0 5
  • 溪水不因山川的阻擋而止步不前,而是適應(yīng)地勢(shì)終究涌入大海。 向日葵不因太陽(yáng)的東升西落而暗自哭泣,而是跟著太陽(yáng)轉(zhuǎn)終究結(jié)...
    花滿(mǎn)筑閱讀 392評(píng)論 0 1

友情鏈接更多精彩內(nèi)容