Android学习笔记之Service应用

Service(服务)是能够在后台执行长时间运行操作并且不提供用户界面的应用程序组件。其他应用程序组件能启动服务,并且即便用户切换到另一个应用程序,服务还可以在后台运行。此外,组件能够绑定到服务并与之交互,甚至执行进程间通信(IPC).

一、Service概述

1.Service的分类

服务从 本质上可以分为以下两种类型

①Started(启动):

当应用程序组件(如activity)通过调用startService()方法启动服务时,服务处于started状态。一旦启动,服务能在后台无限期运行,即使启动它的组件已经被销毁。通常,启动服务执行单个操作并且不会向调用者返回结果。例如,它可能通过网络下载或者上传文件。如果操作完成,服务需要停止自身。

②Bound(绑定):

当应用程序组件通过调用bindService()方法绑定到服务时,服务处于bound状态。绑定服务提供客户端-服务器接口,以允许组件与服务交互、发送请求、获得结果,甚至使用进程间通信(IPC)跨进程完成这些操作。仅当其他应用程序组件与之绑定时,绑定服务才运行。多个组件可以一次绑定到一个服务上,当它们都解绑定时,服务被销毁。

服务也可以同时属于这两种类型,既可以启动(无限期运行)也能绑定。其重点在于是否实现一些回调方法:onStartCommand()方法允许组件启动服务;onBind()方法允许组件绑定服务。

服务运行于管理它的进程的主线程,服务不会创建自己的线程,也不会运行于独立的进程(除非开发人员定义)。这意味着,如果服务要完成CPU密集工作或者阻塞操作,开发人员需要在服务中创建新线程来完成这些工作。通过使用独立的线程,能减少应用程序不响应(ANR)错误的风险,并且应用程序主线程仍然能用于用户与Activity的交互。

2.Service类中的重要方法

为了创建服务,开发人员需要创建Service类(或其子类)的子类。在实现类中,需要重写一些处理服务生命周期重要方面的回调方法,并根据需要提供组件绑定到服务的机制。需要重写的重要回调方法如下:

①onStartCommand()

当其他组件调用startService()方法请求服务启动时,系统调用该方法。一旦该方法执行,服务就启动并在后台无限期运行。如果开发人员实现该方法,则需要在任务完成时调用stopSelf()或stopService()方法停止服务(如果仅想提供绑定,则不必实现该方法)。

②onBind()

当其他组件调用bindService()方法想与服务绑定时(如执行RPC),系统调用该方法。在该方法的实现中,开发人员必须通过返回IBinder提供客户端用来与服务通信的接口。该方法必须实现,但是如果不想允许绑定,则返回null。

③onCreate()

当服务第一次创建时,系统调用该方法执行一次性建立过程(在系统调用onStartCommand()或onBind()方法前)。如果服务已经运行,该方法不被调用。

④onDestroy()

当服务不再使用并即将销毁时,系统调用该方法。服务应该实现该方法来清理诸如线程、注册监听器、接收者等资源。这是服务收到的最后调用。

如果组件调用startService()方法启动服务,服务需要使用stopSelf()方法停止自身,或者其他组件使用stopService()方法停止该服务。

如果组件调用bindService()方法创建,服务运行时间与组件绑定到服务的时间一样长。一旦服务从所有客户端解绑定,系统会将其销毁。

Android系统仅当内存不足并且必须回收系统资源来显示用户关注的activity时,才会强制停止服务。如果服务绑定到用户关注的activity,则会减少停止概率。如果服务被声明为前台运行,则基本不会停止。否则,如果服务是started状态并且长时间运行,则系统会随着时间推移降低其在后台任务列表中的位置并且有很大概率将其停止。如果服务是started状态,则必须设计系统重启服务。系统停止服务后,资源可用时会将其重启(但这也依赖于onStartCommand()方法的返回值)。

3.Service的声明

为了声明Service,需要向<application>标签中添加<service>子标签,<service>子标签的语法如下:

    <service

       android:enabled=["true"|"false"]

       android:exported=["true"|false"]

       android:icon="drawable resource"

       android:label="string resource"

       android:name="string"

       android:permission="string"

       android:process="string"

       ...

    >

    </service>

各个标签属性的说明如下:

①android:enabled

服务能否被系统实例化,true表示可以,false表示不可以,默认值是true。<application>标签也有自己的enabled属性,用于包括服务的全部应用程序组件。<application>和<service>的enabled属性必须同时设置成true(两者的默认值都是true)才能让服务可用。如果任何一个是false,服务被禁用并且不能实例化。

②android:exported

其他应用程序组件能否调用服务或者与其交互,true表示可以,false表示不可以。当该值是false时,只有同一个应用程序的组件或者具有相同用户ID的应用程序能启动或者绑定到服务。

默认值依赖于服务是否包含Intent过滤器。若没有过滤器,服务仅能能通过精确类名调用,这意味着服务仅用于应用程序内部(因为其他程序可能不知道类名)。此时,默认值是false;若存在至少一个过滤器,暗示服务可以用于外部,音质默认值是true。

该属性不是限制其他应用程序使用服务的唯一方式。还可以使用permission属性限制外部实体与服务交互。

③android:icon

表示服务的图标

④android:label

显示给用户的服务名称

⑤android:name

实现服务的Service子类名称,应该是一个完整的类名

⑥android:permission

实体必须包含的权限名称,以便启动或者绑定到服务。

如果startService()、bindService()或者stopService()方法调用者没有被授权,方法调用无效,并且Intent对象也不会发送给服务。

如果没有设置该属性,使用<application>标签的permission属性设置给服务。如果<application>和<service>标签的permission属性都未设置,服务不受权限保护。

⑦android:process

服务运行的进程名称。

通常,应用程序的全部组件运行于为应用程序创建的默认进程。进程名称与应用程序包名相同。

<application>标签的precess属性能为全部组件设置一个相同的默认值。但是组件能用自己的process属性重写默认值,从而允许应用程序跨越多个进程。

如果分配给该属性的名称以冒号(:)开头,仅属于应用程序的新进程会在需要时创建,服务能在该进程中运行;

如果进程名称以小写字母开头,服务会运行在以此为名的全局进程,但需要提供相应的权限。这允许不同应用程序组件共享进程,减少资源使用。

二、创建started Service 

Started Service(启动服务)是由其他组件调用startService()方法启动的,这导致服务的onStartCommand()方法被调用。

Android提供了两个类供开发人员继承以创建启动服务。

①service:

这是所有服务的基类。当继承该类时,创建新线程来执行服务的全部工作是非常重要的。因为服务默认使用应用程序的主线程,这可能降低应用程序activity的运行性能。

②IntentService:

这是service类的子类,它每次使用一个工作线程来处理全部启动请求。在不必同时处理多个请求时,这是最佳选择。开发人员仅需要实现onHandleIntent()方法,该方法接收每次启动请求的Intent以便完成后台任务。

IntentService可完成如下任务:

①创建区别于应用程序主线程的默认工作线程来执行发送到onStartCommand()方法的全部Intent。

②创建工作队列,每次传递一个Intent到onHandleIntent()方法实现,这样就不必担心多线程。

③所有启动请求处理完毕后停止服务,这样就不必调用stopself()方法。

④提供onBind()方法默认实现,其返回值是null。

⑤提供onStartCommand()方法默认实现,它先发送Intent到工作队列,然后到onHandleIntent()方法实现。

以上说明开发人员仅需实现onHandleIntent()方法来完成客户端提供的任务。由于IntentService类没有提供空参数的构造方法,一次需要提供一个构造方法。

如果需要让服务处理多线程,则可以继承Service类来处理各个Intent。由于开发人员自己处理onStartCommand()方法调用,可以同时处理多个请求。

onStartCommand()方法必须返回一个整数。该值用来描述系统停止服务后如何继续服务。onStartCommand()方法返回值必须是下列常量之一。

①START_NOT_STICKY

如果系统在onStartCommand()方法返回后停止服务,不重新创建服务,除非有PendingIntent要发送。为避免不在不需要的时候运行服务,这是最佳选择。

②START_STICKY

如果系统在onStartCommand()方法返回后停止服务,重新创建服务并调用onStartCommand()方法,但是不重新发送最后的Intent;相反,系统使用空Intent调用onStartCommand()方法,除非有PendingIntent来启动服务,此时,这些Intent会被发送。这是和多媒体播放器(或者类似服务),它们不执行命令但是无限期运行并等待工作。

③START_REDELIVER_INTENT

如果系统在onStartCommand()方法返回后停止服务,重新创建服务并使用发送给服务的最后Intent调用onStartCommand()方法,全部PendingIntent依次发送。这适合积极执行应该立即回复工作的服务,如下载文件。

启动服务startService()

停止服务stopSelf()或stopService()        stopSelf(int)

三、创建Bound Service

绑定服务是允许其他应用程序绑定并且与之交互的Service类实现类。为了提供绑定,开发人员必须实现onBind()回调方法。该方法返回IBinder对象,它定义了客户端用来与服务交互的程序接口。

客户端能通过bindService()方法绑定到服务。此时,客户端必须提供ServiceConnection接口的实现类,它监视客户端与服务之间的连接。bindService()方法立即返回,但是当Android系统创建客户端与服务之间的连接时,它调用ServiceConnection接口的onServiceConnected()方法,来发送客户端用来与服务通信的IBinder对象。

多个客户端能同时连接到服务。然而,仅当第一个客户端绑定时,系统调用服务的onBinder()方法来获取IBinder对象。系统接着发送同一个IBinder对象到其他绑定的客户端,但是不在调用onBind()方法。

当最后的客户端与服务解绑定时,系统销毁服务(除非服务也使用startService()方法启动)。

在实现绑定服务时,最重要的是定义onBind()回调方法返回的接口,有以下3种方式。

1.继承Binder类

如果服务对应用程序私有并且与客户端运行于相同的进程(这非常常见),则应该继承Binder类来创建接口,并且从onBind()方法返回其一个实例。客户端接收 Binder对象并用其来直接访问Binder实现类或者Service类中可用公共方法。

当服务仅用于私有应用程序时,推荐使用该技术。但当服务可以用于其他应用程序或者访问独立进程时,则不能使用该技术。

2.使用Messenger

如果需要接口跨进程工作,则可以使用Messenger来为服务创建接口。此时,服务定义Handler对象来相应不同类型的Message对象。Handler是Messenger的基础,能与客户端分享IBinder,允许客户端使用Message对象向服务发送命令。此外,客户端能定义自己的Messenger对象,这样服务能发送回消息。

使用Messenger是执行进程间通信(IPC)的最简单方式,因为Messenger类将所有请求队列化到单独的线程,这样开发人员就不必设计服务为线程安全。

3.使用AIDL

AIDL(Android接口定义语言)执行分解对象到原语的全部工作,以便操作系统能理解并且跨进程执行IPC。使用Messenger创建接口,实际上将AIDL作为底层架构。如上所述,Messenger在单个线程中将所有客户端请求队列化,这样服务每次收到一个请求。如果希望服务能同时处理多个请求,则可以直接使用AIDL。此时,服务必须能处理多线程并且要保证线程安全。

为了直接使用IDL,开发人员必须创建定义编程接口的.aidl文件。Android SDK工具使用该文件来生成抽象类,它实现接口并处理IPC,然后就可以在服务中使用了。

继承Binder类

其实现步骤如下:

1.在服务中,创建Binder类实例来完成下列操作之一:

①包含客户端能调用的公共方法

②返回当前Service实例,其中包含客户端能调用的公共方法。

③返回服务管理的其他类的实例,其中包含客户端能调用的公共方法。

2.从onBind()回调方法中返回Binder类实例。

3.在客户端,从onServiceConnected()回调方法接收Binder类实例,并且使用提供的方法调用绑定服务。

public class LocalService extends Service{

   private final IBinder binder = new LocalBinder();

   private final Random generator = new Random();

   public class LocalBinder extends Binder{

       LocalService getService(){

          return LocalService.this;

       } 

   }

   public IBinder onBind(Intent intent){

      return binder;

    }

    public int getRandomNumber(){

       return generator.nextInt(100);

    }

}

public class BindingActivity extends Activity{

   LocalService localService;

   boolean bound=false;

   

   protected void onCreate(Bundle savedInstanceState){

       super.onCreate(savedInstanceState);

       setContentView(R.layout.main);

   }

   protected void onStart(){

      super.onStart();

      Intent intent = new  Intent(this,LocalService.class);

      bindService(intent,connection,Context.BIND_AUTO_CREATE);

   }

   protected void onStop(){

      super.onStop();

      if(bound){

         unbindService(connection);

        bound=false;

      }

   }

   public void onButtonClick(View v){

      if(bound){

         int num=localService.getRandomNumber();

         Toast.makeText(this,"获得随机数:"+num,Toast.LENGTH_SHORT).show();

      }

   }

   private ServiceConnection connection=new ServiceConnection(){

       public void onServiceConnected(ComponentName className,IBinder service){

          LocalBinder binder = (LocalBinder)service;

          localService=binder.getService();

          bound=true;

       }

       public void onServiceDisconnected(ComponentName arg0){

          bound=false;

       }

   };

}

使用Messenger类

如果开发人员需要服务与远程进程通信,则可以使用Messenger来为服务提供接口。该技术允许不适用AIDL执行进程间通信(IPC).

使用Messenger时需注意:

①实现Handler的服务因为每次从客户端调用而收到回调。

②Handler用于创建Messenger对象(它是Handler的引用)

③Messenger创建IBinder,服务从onBind()方法将其返回到客户端。

④客户端使用IBinder来实例化Messenger,然后使用它来发送Message对象到服务。

⑤服务在其Handler的handleMessage()方法接收Message。

此时,没有供客户端在服务上调用的方法。相反,客户端发送消息到服务的Handler方法。

绑定到服务

应用程序组件(客户端)能调用bindService()方法绑定到服务,接下来Android系统调用服务的onBind()方法,返回IBinder来与服务通信。

绑定是异步的。bindService()方法立即返回并且不返回IBinder到客户端。为了接收IBinder,客户端必须创建ServiceConnection实例,然后将其传递给bindService()方法。ServiceConnection包含系统调用发送IBinder的回调方法。

注意:只有Activity、Service和ContentProvider能绑定到服务,BroadcastReceiver不能绑定到服务。

如果需要从客户端绑定服务,需要完成以下操作:

①实现ServiceConnection,这需要重写onServiceConnected()和onServiceDisconnected()两个回调方法。

②调用bindService()方法,传递ServiceConnection实现。

③当系统调用onServiceConnected()回调方法时,就可以使用接口定义的方法调用服务。

④调用unbindService()方法解绑定。

当客户端销毁时,会将其从服务上解绑定。但是当与服务完成交互或者activity暂停时,最好解绑定,以便系统能够及时停止不用的服务。

四、管理Service的生命周期

1.started Service

2.bound Service

服务的更多技巧

服务几乎都是在后台运行的,一直以来它都是默默地做着辛苦地工作,但是服务地系统优先级还是比较低地,当系统出现内存不足地情况时,就有可能会回收掉正在后台运行地服务。如果你希望服务可以一直保持运行状态,而不会由于系统内存不足原因导致被回收,就可以考虑使用前台服务。前台服务和普通服务地最大区别就在于,它会一直有一个正在运行地图标在系统地状态栏显示,下拉状态栏后可以看到更详细地信息,非常类似于通知地效果。当然有时候你也可能不仅仅时为了防止服务被回收才使用前台服务地,有些项目由于特殊地需求会要求必须使用前台服务,比如彩云天气这款天气预报应用,它地服务在后台更新天气数据地同时,还会在系统状态栏一直显示当前地天气信息。

那么我们就来看以下如何才能创建一个前台服务吧!

public class MyService extends Service{

    ...

    public void onCreate(){

        super.onCreate();;

        Log.d("MyService","onCreate executed");

        Intent intent = new Intent(this,MainActivity.class);

        PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);

        Notification notification = new NotificationCompat.Builder(this)

                       .setContentTitle("This is content title“)

                       .setContentText("This is content text")

                       .setWhen(System.currentTimeMillis())

                       .setSmallIcon(R.mipmap.ic_launcher)

                       .setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher)

                       .setContentIntent(pi)

                       .build();

       startForeground(1,notification);

    }

    ...

}

可以看到,这里只是修改了onCreate()方法中地代码,这次在构建出Notification对象后并没有使用NotificationManager来将通知显示出来,而是调用了startfForeground()方法,这个方法接收两个参数,第一个参数时通知的id,类似于notify()方法的第一个参数,第二个参数则是构建出Notification对象,调用startForeground()方法后就会让MyService变成一个前台服务,并在系统状态栏显示出来。

服务的最佳实践——完整版的下载示例

添加依赖库:

dependencies{

    compile fileTree(dir:'libs‘,include:['*.jar'])

    compile 'com.android.support:appcompat-v7:24.2.1'

    testCompile 'junit:junit:4.12'

    compile 'com.squareup.okhttp3:okhttp;3.4.1‘

}

接下来定义一个回调接口,用于对下载过程中的各种状态进行监听和回调,新建一个DownloadListener接口,代码如下:

public interface DownloadListener{

    void onProgress(int progress);

    void onSuccess();

    void onFailed();

    void onPaused();

    void onCanceled();

}

可以看到,这里我们一共定义了5个回调方法,onProgress()方法用于通知当前的下载进度,onSuccess()方法用于通知下载成功事件,onFailed()方法用于通知下载失败事件,onPaused()方法用于通知下载暂停事件,onCanceled()方法用于通知下载取消事件。

开始编写下载功能了,用AsyncTask来进行实现,新建一个DownloadTask继承子AsyncTask,代码如下:

public class DownloadTask extends AsyncTask<String ,Integer,Integer>{

    public static final int TYPE_SUCCESS=0;

    public static final int TYPE_FAILED=1;

    public static final int TYPE_PAUSED=2;

    public static final int TYPE_CANCELED=3;

    private DownloadListener listener;

    private boolean isCanceled=false;

    private boolean isPaused=false;

    private int lastProgress;

    public DownloadTask(DownloadListener listener){

       this.listener = listener;

    }

  

    protected Integer doInBackground(String... params){

        InputStream is=null;

        RandomAccessFile savedFile=null;

        File file=null;

        try{

            long downloadedLength=0;// 记录已下载的文件长度

            String downloadUrl=params[0];

            String fileName=downloadUrl.substring(downloadUrl.lastIndexOf("/"));

            String directory=Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();

            file = new File(directory+fileName);

            if(file.exists()){

                downloadedLength = file.length();

            }

            long contentLength = getContentLength(downloadUrl);

            if(contentLength==0){

                return TYPE_FAILED;

            }else if(contentLength==downloadedLength){

               //已下载字节和文件总字节相等,说明已经下载完成了

               return TYPE_SUCCESS;

            }

            OkHttpClient client = new OkHttpClient();

            Request request = new Request.Builder()

                           //断点下载,指定从哪个字节开始下载

                          .addHeader("RANGE",’bytes="+downloadedLength+"-")

                          .url(downloadUrl)

                          .build();

             Response response = client.newCall(request).execute();

             if(resonse!=null){

                is=resonse.body().byteStream();

                savedFile = new RandomAccessFile(file,"rw");

                savedFile.seek(downloadedLength);//跳过已下载的字节

                byte[] b = new byte[1024];

                int total=0;

                int len;

                while((len=is.read(b))!=-1){

                    if(isCanceled){

                       return TYPE_CANCELED;

                    }else if(isPaused){

                       return TYPE_PAUSED;

                    }else{

                       tottal+=len;

                       savedFile.write(b,0,len);

                       //计算已下载的百分比

                      int progress = (int)((total+downloadedLength)*100/contentLength);

                      publishProgress(progress);

                     }

                   response.body().close();

                   return TYPE_SUCCESS;

                }

              }catch(Exception e){

                 e.printStackTrace();

              }finally{

                  try {

                      if(!is;=null){is.close()}

                      if(savedFile!=null){savedFile.close();}

                      if(isCanceled&&file!=null){file.delete();}

                  } catch(Exception e){

                      e.printStackTrace();

                  }

             }

             return TYPE_FAILED;

        }

    

    protected void onProgressUpdate(Integer.. values){

        int progress=values[0];

        if(progress>lastProgress){

           listener.onProgress(progress);

           lastProgress=progress;

         }

     }

    protected void onPostExecute(Integer status){

        case TYPE_SUCCESS:

            listener.onSuccess();

            break;

        case TYPE_FAILED:

            listener.onFailed();

            break;

        case TYPE_PAUSED:

            listener.onPaused();

            break;

         case TYPE_CANCELED:

             listener.onCanceled();

         default:break;

    }

    public void pauseDownload(){

       isPaused=true;

    }

    public void cancelDownload(){

       isCanceld=true;

    }

   

    private long getContentLength(String downloadUrl) throws IOException{

        OkHttpClient client = new OkHttpClient();

         Request request = new Request.Builder().url(downloadUrl).build();

        Response response = client.newCall(request).execute();

        if(response!=null&&response.isSuccessful()){

            long contentLength = response.body().contentLength();

            response.body()..close();

            return contentLength;

         }

         return 0;

    }

}

创建下载服务

public class DownloadService extends Service{

    private DownloadTask downloadTask;

    private String downlaodUrl;

    private DownloadListener listener = new DownloadListener(){

        public void onProgress(int progress){

            getNotificationManager().notify(1,getNotification("Downloading..",progress));

        }

        public void onSuccess(){

            dwonloadTask=null;

            stopForeground(true);

            getNotificationManager.notify(1,getNotification("Download Success",-1));

            Toast.makeText(DownloadService.this,"Download Success",Toast.LENGTH_SHORT).show();

        }

        public void onFailed(){

            downloadTsk=null;

            stopForeground(true);

            getNotificationManager().notify(1,getNotification("Download Failed",-1));

            Toast.makeText(DownloadService.this,"Download Failed",Toast.LENGTH_SHORT).show();

        }

        public void onPaused(){

            downloadTask=null;Toast.makeText(DownloadService.this,"Paused",Toast.LENGTH_SHORT).show();

        }

        public void onCanceled(){

            downlaodTask=null;

             stopForeground(true);

            Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_SHORT).show();

         }

    };

    private DownloadBinder mBinder = new DownloadBinder();

    public IBinder onBind(Intent intent){

        return mBinder;

    }

    class DownloadBinder extends Binder{

        public void startDownload(String url){

            if(downloadTask==null){

                downloadUrl=url;

                downloadTask = new DownloadTask(listener);

                downloadTask.execute(downloadUrl);

                startForeground(1,getNotification("Downloading...",0));

                Toast.makeText(DownloadService.this,"Downloading...",Toast.LENGTH_SHORT).show();

            }

        }

        public void pauseDownload(){

            if(downloadTask!=null){

               downloadTask.pauseDownload();

            }

        }

        public void cancelDownload(){

           if(downloadTask!=null){

               downloadTask.cancelDownload();

           }else{

              if(downloadUrl!=null){

                  String fileName=downlaodUrl..substring(downloadUrl.lastIndexOf("/"));

                  String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();

                  File file = new File(directory+fileName);

                  if(file.exists()){

                      file.delete();

                  }

                  getNotificationManager().cancel(1);

                  stopForeground(true);

                  Toast.makeText(DownloadService.this,"Canceled",Toast,LENGTH_SHORT).show();

              }

           }

        }

    }

    private NotificationManager getNotificationManager(){

        return (NotificationManager)getSystemService(NOTIFICATION_SERVICE);

    }

    private Notification getNotification(String title,int progress){

       Intent intent  = new Intent(this,MainActivity.class);

       PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);

       NotificationCompat.Builder buidler = new NotificationCompat.Builder(this);

       builder.setSmallIcon(R.mipmap.ic_launcher);

       builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher));

       builder.setContentIntent(pi);

       builder.setContentTile(title);

       if(progress>0){

           builder.setContentText(progress+"%");

           builder.setProgress(100,progress,false);//第一个参数传入通知的最大进度,第二个参数传入通知的当前进度,第三个参数表示是否使用模糊进度条,这里false显示进度

       }

       return builder.build();

    }

}

MainActivity代码如下:

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private DownloadService.DownloadBinder downloadBinder;

   private  ServiceConnection connection = new ServiceConnection(){

       public void onServiceDisconnected(ComponentName name){

       

       }

       public void onServiceConnected(ComponentName name,IBinder service){

           downloadBinder = (DownloadService.DownloadBinder)service;

       }

   };

    protected void onCreate(Bundle savedInstanceState){

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        Button startDownload = (Button) findViewById(R.id.start_download);

        Button pauseDownlaod = (Button) findViewById(R.id.pause_download);

        Button cancelDownload = (Button)findViewById(R.id.cancel_download);

        startDownload.setOnClickListener(this);

        cancelDownload.setOnClickListener(this);

        pauseDownload.setOnClickListener(this);

        Intent intent = new Intent(this,DownloadService.class);

        startService(intent);

         bindService(intent,connection,BIND_AUTO_CREATE);

        if(ContextCompat.checkSelfPermission(MainActivity.this,Manifest.permission.WRITE_EXTERNAL_STORAGE)!=PackageManager.PERMISSION_GRANTED){

           ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);

        }

    }

    public void onClick(View v){

        if(downloadBinder==null{return;}

        switch(v.getId()){

            case R.id.start_download:

                String url = "https://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe";

                downloadBinder.startDownload(url);

                break;

            case R.id.pause_download:

                downloadBinder.pauseDownload();

                break;

           case R.id.cancel_download:

               downloadBinder.cancelDownload();

               break;

           default:break;

        }

    }

    public void onRequestPermissionsResult(int requestCode,String[] permissions,int[] grantResults){

        switch(requestCode){

            case 1:

                      if(grantResults.length>0&&grantResults[0]!=PackageManager.PERMISSION_GRANTED){

                          Toast.makeText(this,"拒绝权限将无法使用程序",Toast.LENTH_SHORT).show();

                           finish();

                      }

                      break;

             default:

        }

    }

    protected void onDestroy(){

        super.onDestroy();

        unbindService(connection);

    }

}

在程序中我们分别调用了startService()和bindService()方法来启动和绑定服务。这一点至关重要,因为启动服务可以保证DownloadService一直在后台运行,绑定服务则可以让MainActivity和DownloadService进行通信。

附:android:process

正常情况下,一个apk启动后只会运行在一个进程中,其进程名为apk的包名,所有的组件都会在这个进程中运行,以下为DDMS的进程截屏:

这里写图片描述

com.biyou.multiprocess为进程名,也是apk的包名,


但是如果需要将某些组件(如Service,Activity等)运行在单独的进程中,就需要用到android:process属性了。我们可以给android的组件设置android:process属性来使其运行在指定的进程中。

  • AndroidMantifest.xml中的activity、service、receiver和provider均支持android:process属性
  • 设置该属性可以使每个组件均在各自的进程中运行,或者使一些组件共享一个进程
  • AndroidMantifest.xml中的application元素也支持android:process属性,可以修改应用程序的默认进程名(默认值为包名)

为何要使用多进程

1.分散内存的占用



我们知道Android系统对每个应用进程的内存占用是有限制的,而且占用内存越大的进程,通常被系统杀死的可能性越大。让一个组件运行在单独的进程中,可以减少主进程所占用的内存,避免OOM问题,降低被系统杀死的概率,

2.实现多模块



比如我做的应用大而全,里面肯定会有很多模块,假如有地图模块、大图浏览、自定义WebView等等(这些都是吃内存大户),还会有一些诸如下载服务,监控服务等等,一个成熟的应用一定是多模块化的。

当我们的应用开发越来越大,模块越来越多,团队规模也越来越大,协作开发也是个很麻烦的事情。项目解耦,模块化,是这阶段的目标。通过模块解耦,开辟新的进程,独立的JVM,来达到数据解耦目的。模块之间互不干预,团队并行开发,责任分工也明确。

3.子进程奔溃,主进程可以继续工作

如果子进程因为某种原因崩溃了,不会直接导致主程序的崩溃,可以降低我们程序的崩溃率。
 

4.主进程退出,子进程可以继续工作

即使主进程退出了,我们的子进程仍然可以继续工作,假设子进程是推送服务,在主进程退出的情况下,仍然能够保证用户可以收到推送消息。
 

5.实现守护进程



如果主线程中的服务要从开机起持续运行,若由于内存等原因被系统kill掉,守护进程可以重新启动主线程的服务。

通过JNI利用C/C++,调用fork()方法来生成子进程,一般开发者会利用这种方法来做一些daemon(守护进程)进程,来实现防杀保活等效果。

另外:

还能通过监控进程,将这个错误上报给系统,告知他在什么机型、环境下、产生了什么样的Bug,提升用户体验。

实现

1 . 如果android:process的值以冒号开头的话,那么该进程就是私有进程,如下:

配置:

<application
   ……
   <service android:name=".ProcessTestService" android:process=":secondProcess"/>
   ……
</application>
  • 1
  • 2
  • 3
  • 4
  • 5

进程:

这里写图片描述

2 . 以小写字母开头(如com.secondProcess),那么就是公有进程,android:process值一定要有个点号:

不能以数字开头,并且要符合命名规范,必须要有.否则将会出现这种错误: Invalid process name simon in package com.wind.check: must have at least one ‘.’
配置:

<application
   ……
   <service android:name=".LocalService" android:process="com.secondProcess"/>
   ……
</application>

进程:

这里写图片描述

3 . 私有进程和公有进程的区别:
android:process=":remote",以冒号开头,冒号后面的字符串原则上是可以随意指定的。如果我们的包名为“com.biyou.multiprocess”,则实际的进程名
为“com.biyou.multiprocess:remote”。这种设置形式表示该进程为当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中
全局进程
进程名称不以“:”开头的进程都可以叫全局进程,如android:process="com.secondProcess",以小写字母开头,表示运行在一个以这个名字命名的全局进程中,其他应用通过设置相同的ShareUID可以和它跑在同一个进程

ps:ShareUID :
ShareUserId,在Android里面每个app都有一个唯一的linux user ID,则这样权限就被设置成该应用程序的文件只对该用户可见,只对该应用程序自身可见,而我们可以使他们对其他的应用程序可见,这会使我们用到SharedUserId,也就是让两个apk使用相同的userID,这样它们就可以看到对方的文件。为了节省资源,具有相同ID的apk也可以在相同的linux进程中进行(注意,并不是一定要在一个进程里面运行),共享一个虚拟机。
ShareUserId的作用,数据共享、调用其他程序资源。

进程生命周期与优先级

Android 系统将尽量长时间地保持应用进程,但为了新建进程或运行更重要的进程,最终需要移除旧进程来回收内存。 为了确定保留或终止哪些进程,系统会根据进程中正在运行的组件以及这些组件的状态,将每个进程放入“重要性层次结构”中。 必要时,系统会首先消除重要性最低的进程,然后是重要性略逊的进程,依此类推,以回收系统资源。

重要性层次结构一共有 5 级。以下列表按照重要程度列出了各类进程(第一个进程最重要,将是最后一个被终止的进程):

1.前台进程:(foreground process)

用户当前操作所必需的进程。如果一个进程满足以下任一条件,即视为前台进程:
托管用户正在交互的 Activity(已调用 Activity 的 onResume() 方法)
托管某个 Service,后者绑定到用户正在交互的 Activity
托管正在“前台”运行的 Service(服务已调用 startForeground())
托管正执行一个生命周期回调的 Service(onCreate()、onStart() 或 onDestroy())
托管正执行其 onReceive() 方法的 BroadcastReceiver
通常,在任意给定时间前台进程都为数不多。只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。 此时,设备往往已达到内存分页状态,因此需要终止一些前台进程来确保用户界面正常响应。

2.可见进程

没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。 如果一个进程满足以下任一条件,即视为可见进程:
托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。例如,如果前台 Activity 启动了一个对话框,允许在其后显示上一 Activity,则有可能会发生这种情况。
托管绑定到可见(或前台)Activity 的 Service。
可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。

3.服务进程
正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。

4.后台进程
包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。 有关保存和恢复状态的信息,请参阅 Activity文档。

5.空进程
不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。
根据进程中当前活动组件的重要程度,Android 会将进程评定为它可能达到的最高级别。例如,如果某进程托管着服务和可见 Activity,则会将此进程评定为可见进程,而不是服务进程。

此外,一个进程的级别可能会因其他进程对它的依赖而有所提高,即服务于另一进程的进程其级别永远不会低于其所服务的进程。 例如,如果进程 A 中的内容提供程序为进程 B 中的客户端提供服务,或者如果进程 A 中的服务绑定到进程 B 中的组件,则进程 A 始终被视为至少与进程 B 同样重要。

由于运行服务的进程其级别高于托管后台 Activity 的进程,因此启动长时间运行操作的 Activity 最好为该操作启动服务,而不是简单地创建工作线程,当操作有可能比 Activity 更加持久时尤要如此。例如,正在将图片上传到网站的 Activity 应该启动服务来执行上传,这样一来,即使用户退出 Activity,仍可在后台继续执行上传操作。使用服务可以保证,无论 Activity 发生什么情况,该操作至少具备“服务进程”优先级。 同理,广播接收器也应使用服务,而不是简单地将耗时冗长的操作放入线程中。

注意的地方

有一点一定要记住:进程间的内存空间是不可见的。从而,开启多进程后,我们需要面临这样几个问题:

1. Application的多次重建。

Manifest文件如上面提到的,定义了两个类:ProcessTestActivity和ProcessTestService,我们只是在Activity的onCreate方法中直接启动了该Service,同时,我们自定义了自己的Application类。代码如下:

public class MyApplication extends Application {
    public static final String TAG = "viclee";
    @Override
    public void onCreate() {
        super.onCreate();
        int pid = android.os.Process.myPid();
        Log.d(TAG, "MyApplication onCreate");
        Log.d(TAG, "MyApplication pid is " + pid);
    }
}
public class ProcessTestActivity extends Activity {
    public final static String TAG = "viclee";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_process_test);

        Log.i(TAG, "ProcessTestActivity onCreate");
        this.startService(new Intent(this, ProcessTestService.class));
    }
}
public class ProcessTestService extends Service {
    public static final String TAG = "viclee";

    @Override
    public void onCreate() {
        Log.i(TAG, "ProcessTestService onCreate");
    }

    @Override
    public IBinder onBind(Intent arg0) {
        return null;
    }

}


 

这里写图片描述
 

  我们发现MyApplication的onCreate方法调用了两次,分别是在启动ProcessTestActivity和ProcessTestService的时候,而且我们发现打印出来的pid也不相同。由于通常会在Application的onCreate方法中做一些全局的初始化操作,它被初始化多次是完全没有必要的。出现这种情况,是由于即使是通过指定process属性启动新进程的情况下,系统也会新建一个独立的虚拟机,自然需要重新初始化一遍Application。那么怎么来解决这个问题呢?
下面给出解决方案:

思路:判断是否为主进程,只有主进程的时候才执行下面的操作

String processName = this.getProcessName();

//判断进程名,保证只有主进程运行
if (!TextUtils.isEmpty(processName) &&processName.equals(this.getPackageName())) {
    //在这里进行主进程初始化逻辑操作                          
    Log.i(">>>>>>","oncreate");
}

获取进程名的方法,这个方法是效率最好的:

 public static String getProcessName() {
        try {
            File file = new File("/proc/" + android.os.Process.myPid() + "/" + "cmdline");
            BufferedReader mBufferedReader = new BufferedReader(new FileReader(file));
            String processName = mBufferedReader.readLine().trim();
            mBufferedReader.close();
            return processName;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

2. 静态成员的失效。

将之前定义的Activity和Service的代码进行简单的修改,代码如下:

public class ProcessTestActivity extends Activity {
    public final static String TAG = "viclee";
    public static boolean processFlag = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_process_test);

        processFlag = true;
        Log.i(TAG, "ProcessTestActivity onCreate");
        this.startService(new Intent(this, ProcessTestService.class));
    }
}
public class ProcessTestService extends Service {
    public static final String TAG = "viclee";

    @Override
    public void onCreate() {
        Log.i(TAG, "ProcessTestService onCreate");
        Log.i(TAG, "ProcessTestActivity.processFlag is " + ProcessTestActivity.processFlag);
    }

    @Override
    public IBinder onBind(Intent arg0) {
        return null;
    }

}

重新执行代码,打印Log :
 

这里写图片描述

  从上面的代码和执行结果看,我们在Activity中定义了一个标志processFlag并在onCreate中修改了它的值为true,然后启动Service,但是在Service中读到这个值却为false。按照正常的逻辑,静态变量是可以在应用的所有地方共享的,但是设置了process属性后,产生了两个隔离的内存空间,一个内存空间里值的修改并不会影响到另外一个内存空间。

3. 文件共享问题。

  多进程情况下会出现两个进程在同一时刻访问同一个数据库文件的情况。这就可能造成资源的竞争访问,导致诸如数据库损坏、数据丢失等。在多线程的情况下我们有锁机制控制资源的共享,但是在多进程中比较难,虽然有文件锁、排队等机制,但是在Android里很难实现。解决办法就是多进程的时候不并发访问同一个文件,比如子进程涉及到操作数据库,就可以考虑调用主进程进行数据库的操作。

参考:
1. 这可能是最全的Android:Process (进程)讲解了
2. Android应用内多进程分析和研究

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值