Android中的服务

Android中的服务

四大组件都是运行在主线程

Android中的服务,是在后台运行 。可以理解成是在后台运行并且是没有界面的Activity。

  • Foreground process 前台进程 ,用户正在交互 ,可以理解成相当于Activity执行onResume方法
  • Visible process 可视进程,用户没有在交互,但用户还一直能看得见页面。相当于Activity执行了onPause方法
  • Service Process 服务进程 ,通过startService()开启了一个服务
  • Background process 后台进程。当前用户看不见页面,相当于Activity执行了onStop方法
  • Empty process 空进程

以上五种进程,优先级依次降低

服务的简单例子

定义一个类继承Service。

package com.example.servicedemo;
 
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
 
public class MyService extends Service {
 
    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }
 
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d("service", "onStartCommand: ");
        return super.onStartCommand(intent, flags, startId);
    }
 
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d("service", "onCreate");
 
    }
 
    @Override
    public void onDestroy() {
        Log.d("service", "onDestroy");
        super.onDestroy();
    }
}
 

重写了onStartCommand、onCreate、onDestroy

布局文件



 
    

MainActivity

package com.example.servicedemo;
 
import android.content.Context;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
 
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
    private Context mContext;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = this;
 
        Button btStartService = (Button) findViewById(R.id.bt_start_service);
        Button btStopService = (Button) findViewById(R.id.bt_stop_service);
        btStartService.setOnClickListener(this);
        btStopService.setOnClickListener(this);
    }
 
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.bt_start_service:
                Intent startIntent = new Intent(mContext, MyService.class);
                startService(startIntent);
                break;
            case R.id.bt_stop_service:
                Intent stopIntent = new Intent(mContext, MyService.class);
                stopService(stopIntent);
                break;
            default:
        }
    }
}

就是两个按钮,一个用于开启服务,另外一个用于停止服务。

多次点击开启服务按钮,发现onCreate方法只会在第一次点击执行,而onStartCommand方法每次点击都会执行一次,在点击一次停止服务后,调用onDestroy方法停止了服务。

startService 方式开启服务,服务就会在后台长期运行,直到用户手动停止(设置-Apps里面) 或者调用StopService方法,服务才会被销毁。

电话窃听案例

注意:这样的例子仅仅学习的时候可尝试。这种窃听行为是违法的。

服务部分

package com.example.servicedemo;
 
import android.app.Service;
import android.content.Intent;
import android.media.MediaRecorder;
import android.os.Environment;
import android.os.IBinder;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
 
import java.io.IOException;
 
public class MyService extends Service {
 
    private MediaRecorder recorder;
 
    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }
 
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d("service", "onStartCommand: ");
        return super.onStartCommand(intent, flags, startId);
    }
    // 服务第一次开启时调用
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d("service", "onCreate");
        // 1. 获取电话管理实例
        TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
        // 2. 注册一个电话状态的监听
        telephonyManager.listen(new MyPhoneStateListener(), PhoneStateListener.LISTEN_CALL_STATE);
 
    }
 
    private class MyPhoneStateListener extends PhoneStateListener {
        @Override
        public void onCallStateChanged(int state, String incomingNumber) {
            switch (state) {
                // 空闲状态
                case TelephonyManager.CALL_STATE_IDLE:
                    if (recorder != null) {
                        recorder.stop();
                        recorder.reset(); // 使得对象复用,重新开始录音机到空闲状态,从setAudioSource这一步开始
                        recorder.release(); // 释放相关资源,最好在录音完成后调用
                        Log.d("recorder", "录音完成");
 
                    }
                    break;
                // 响铃状态(未接通)
                case TelephonyManager.CALL_STATE_RINGING:
                    Log.d("recorder", "来自"+incomingNumber+"电话...");
                    break;
                // 已接通
                case TelephonyManager.CALL_STATE_OFFHOOK:
                    //[1]实例化对象
                    recorder = new MediaRecorder();
                    //[2]设置音频的来源,这里是麦克风
                    recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
                    //[3]设置音频的输出格式,使用小米5s,默认MPEG-4
                    recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
                    //[4]设置音频的编码方式,默认AMR-NB,这里用AAC编码
                    recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
                    //[5]保存的文件路径,为了号召放在了SD卡根目录下。m4a是MPEG-4的音频格式
                    recorder.setOutputFile(Environment.getExternalStorageDirectory().getPath()+"/"+System.currentTimeMillis()+"record.m4a");
                    //[5]准备录音
                    try {
                        recorder.prepare();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    Log.d("recorder", "准备录音");
                    recorder.start();
                    Log.d("recorder", "开始录音");
                    break;
                default:
            }
        }
    }
 
    @Override
    public void onDestroy() {
        Log.d("service", "onDestroy");
        super.onDestroy();
    }
}
 

权限申请,注意这三个都是危险权限




布局文件



 
    

MainActivity里面动态申请上面的三个权限

package com.example.servicedemo;
 
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
 
import java.util.ArrayList;
import java.util.List;
 
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
    private Context mContext;
    private List permissions = new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = this;
        // 读取电话状态是危险权限
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
            permissions.add(Manifest.permission.READ_PHONE_STATE);
        }
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
        }
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
            permissions.add(Manifest.permission.RECORD_AUDIO);
        }
        if (permissions.size() > 0) {
            // list.toArray()返回Object[],不能直接转成String[],只能对里面的每个Object转成String,不是很方便。使用下面带参方法
            ActivityCompat.requestPermissions(MainActivity.this,  permissions.toArray(new String[permissions.size()]), 1);
        }
 
        Button btStartService = (Button) findViewById(R.id.bt_start_service);
        Button btStopService = (Button) findViewById(R.id.bt_stop_service);
        btStartService.setOnClickListener(this);
        btStopService.setOnClickListener(this);
    }
 
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.bt_start_service:
                Intent startIntent = new Intent(mContext, MyService.class);
                startService(startIntent);
                break;
            case R.id.bt_stop_service:
                Intent stopIntent = new Intent(mContext, MyService.class);
                stopService(stopIntent);
                break;
            default:
        }
    }
}

点击按钮开启服务后,每次打电话都会录音到本地了。

在服务中注册广播接收者

操作特别频繁的广播事件,屏幕的关闭和打开 ,电量的变化等广播接收器静态注册无效。

而动态注册的话,活动一旦被销毁,就不能接收到广播了。为此,我们可以在服务中注册广播接收者,让其在服务停止时才注销广播接收者。这样使得广播接收者的生命周期变长。

这个例子就监听屏幕的熄灭和点亮。

首先写一个服务,已自动注册到清单文件。

package com.example.broadcastinservice;
 
import android.app.Service;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.IBinder;
 
public class ScreenService extends Service {
    private ScreenReceiver receiver;
 
    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }
 
    @Override
    public void onCreate() {
        super.onCreate();
        // 服务一开启,就注册广播接收者
        receiver = new ScreenReceiver();
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
        intentFilter.addAction(Intent.ACTION_SCREEN_ON);
        registerReceiver(receiver, intentFilter);
    }
 
    @Override
    public void onDestroy() {
        // 服务终止时候才注销广播接收者
          unregisterReceiver(receiver);
        super.onDestroy();
    }
}
 

然后是广播接收者

package com.example.broadcastinservice;
 
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
 
public class ScreenReceiver extends BroadcastReceiver {
 
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (Intent.ACTION_SCREEN_ON.equals(action)) {
            Log.d("Screen", "屏幕点亮了");
        } else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
            Log.d("Screen", "屏幕熄灭了");
        }
    }
}
 

MainActivity

package com.example.broadcastinservice;
 
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
 
public class MainActivity extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 在活动里开启服务,在服务里注册广播接收者
        Intent intent= new Intent(MainActivity.this, ScreenService.class);
        startService(intent);
    }
}
 

服务和活动进行通信

之前使用startService的方法只是通知了服务开始运行,之后他们就再也没有关系了,活动销毁也和服务无关。为了让活动服务的关系更加紧密,具体来说可以让活动能指挥服务。这会用到之前一直没有用过的onBind方法,该方法返回一个IBinder,为此我们需要写个类继承它,然后返回其实例。

service的代码

package com.example.bindservicetest;
 
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;
 
public class MyService extends Service {
 
    private MyBinder myBinder = new MyBinder();
 
    @Override
    public IBinder onBind(Intent intent) {
        Log.d("Binder", "onBind: ");
 
        // 这个返回的binder就是活动里onServiceConnected(ComponentName name, IBinder service)里的service
        // 这样就能在活动里调用服务的方法的
        return myBinder;
    }
 
    class MyBinder extends Binder {
        public void dowload() {
            Toast.makeText(getApplicationContext(), "调用了Binder的download方法", Toast.LENGTH_SHORT).show();
        }
 
        public void cancel() {
            Toast.makeText(getApplicationContext(), "调用了Binder的download方法", Toast.LENGTH_SHORT).show();
        }
    }
 
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d("Binder", "onCreate ");
 
    }
 
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d("Binder", "onStartCommand: ");
        return super.onStartCommand(intent, flags, startId);
    }
 
    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d("Binder", "onDestroy ");
 
    }
}
 

关键的地方,假如上述的downloadcancel方法存在于Service中,里面用到了Toast方法,这个方法必须要有上下文Context才能用,否则报错空指针。虽然Service也是继承于Context,但是如果这个service是被new出来的,那么它只能被当成一个普通的类来看待。不具备上下文环境, 会报空指针异常。

所以bindService的方法应运而生,由此我们就能调用上述方法。

然在活动里,关键是bindService(bindIntent, connection, BIND_AUTO_CREATE);connection需要重写,这里就不继承了,写成匿名类的形式。

package com.example.bindservicetest;
 
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
 
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
 
    private MyService.MyBinder binder;
 
    private ServiceConnection connection;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
          connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.d("Binder", "与服务连接成功");
            // 向下转型
            binder = (MyService.MyBinder) service;
            // 连接到服务后,可以在活动里面调用服务的方法
            binder.dowload();
            binder.cancel();
        }
 
        @Override
        public void onServiceDisconnected(ComponentName name) {
          // 该方法在正常解绑时候不会被调用,只在异常销毁时候调用
            Log.d("Binder", "与服务断开连接");
        }
    };
        Button btBindService = (Button) findViewById(R.id.bt_bind_service);
        btBindService.setOnClickListener(this);
    }
 
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.bt_bind_service:
                Intent bindIntent = new Intent(MainActivity.this, MyService.class);
                bindService(bindIntent, connection, BIND_AUTO_CREATE);
                break;
            default:
        }
    }
 
    @Override
    protected void onDestroy() {
        // 绑定的服务随着活动销毁而停止,活动一但销毁,必须解绑,否则会造成内存泄漏
        unbindService(connection);
        super.onDestroy();
    }
}

上面的代码,只是绑定服务和解绑服务。当解绑服务的时候,会调用服务的onDestroy方法。但是如果同时调用了startServicebindService,则只unbindService是不会停止服务的,即不会调用服务的onDestroy方法,必须调用stopService方法才能停止服务。又因为解绑服务是必须的,所以如果遇到同时调用了bindServicestartService,则同时调用unbindServicestopService才能终止服务。

一般的调用顺序如下:

  1. 先启动服务startService
  2. 再将活动与服务绑定bindService
  3. 解绑服务unbindService
  4. 按需求选择性调用stopService,比如音乐播放器,下载器最好不停止服务。

有几点注意下:

  • 通过bindService方式开启服务,服务不能再设置页面里面找到,相当于是一个隐形的服务。
  • 多次点击绑定服务按钮,也只会执行一次。依次执行onCreate,onBind,onServiceConnected。和startService不一样,不会执行onStartCommand方法。
  • unbindService只能解绑一次,多次解绑会报错。
  • 绑定的服务随着活动的销毁而停止(仅仅绑定而没有startService),活动一旦销毁,服务也会停止。可以在活动未销毁时手动调用unbindService一般在活动的onDestroy方法里面执行解绑操作。避免内存泄漏。

启动服务和绑定服务的区别

  • startService : 让服务长期运行在后台,但是无法与服务进行通讯
  • bindServcie : 可以与服务进行通讯,但是无法长期运行在后台

AIDL介绍

  • 远程服务 运行在其他应用里面的服务
  • 本地服务 运行在自己应用里面的服务

如果在本应用里想调用其他应用的服务,比如付款时候调用支付宝。需要实现进行进程间通信, 又称为IPC。

aidl:Android interface Defination Language,Android接口定义语言,专门是用来解决进程间通信的。

实现一个跨程序调用服务方法的例子,生活中常有这样的例子,比如在美团买美食,付款的时候可以使用支付宝,调用的方法自然不是美团的。所以跨程序了,调用了支付宝的支付服务。

步骤

远程端

Android Studio下,在main下,file -> New里找到AIDL,新建。可以看到,与MainActivity实在同一个包(文件夹)下的。写法和接口类似,要注意的是不要加诸如public的修饰符

// IPayAidlInterface.aidl
package com.example.remoteservice;
 
// Declare any non-default types here with import statements
 
interface IPayAidlInterface {
    boolean pay(String username, String password, int money);
}
 

再写一个支付服务,需要注意的是MyBinder继承了IPayAidlInterface.Stub,这个Stub是Android Studio帮我们自动生成的,不要修改。IPayAidlInterface.Stub同时实现了IPayAidlInterface接口和继承了Binder。

本地端调用pay方法的时候,就会执行远程端PayService里这个已具体实现的pay方法。

package com.example.remoteservice;
 
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
 
public class PayService extends Service {
    private MyBinder myBinder = new MyBinder();
    @Override
    public IBinder onBind(Intent intent) {
        return myBinder;
    }
    // 根据aidl自动生成的Stub,既继承了Binder又实现了接口,extends android.os.Binder implements com.example.remoteservice.IPayAidlInterface
    private class MyBinder extends IPayAidlInterface.Stub {
 
        @Override
        public boolean pay(String username, String password, int money) throws RemoteException {
            if ("admin".equals(username) && "123456".equals(password)) {
                Log.d("Remote", "支付X已收到您的付款,共"+money+"元!");
                return true;
            } else {
                return false;
            }
        }
    }
}
 

在服务里最好不要写Toast,否则报错``Can't create handler inside thread that has not called Looper.prepare()`,原因好像是子线程是没有消息队列的

另外,由于跨程序,不能通过显式Intent通过制定PayService.class来开启服务,必须在AndroidManifest.xml里制定action


  
    
  

远程端这样就算写好了,接下来写本地端。

本地端

首先将远程端的main目录下的aidl文件夹直接复制到本地端,没错,直接复制,啥都不要改,这样可保证两个aidl的包名一样。

界面就是输入账号和密码,点击付款。



    
 
    
 
    

MainActivity,尤其注意一点,Android 5.0之后,开启服务的意图(Intent)必须明确,否则会报错 -> Service Intent must be explicit。添加一句 intent.setPackage("com.example.remoteservice");里面填上要调用的远程服务的包名。

package com.example.localservice;
 
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
 
import com.example.remoteservice.IPayAidlInterface;
 
public class MainActivity extends AppCompatActivity {
 
    private ServiceConnection connection;
    private IPayAidlInterface myPay;
    private EditText etUser;
    private EditText etPassword;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        connection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                // 这部很关键,参数service应该是MyBinder。但是在本程序中自然访问不到它。通过调用从远程端复制过来的aidl的asInterface方法,可获得这个接口。
                // 通过这个接口暴露出来的方法,可以执行myBinder中已实现的方法
                // 和上面直接向下转型MyBinder不一样,这里直接使用的接口而非实现接口后具体的类
                myPay = IPayAidlInterface.Stub.asInterface(service);
            }
 
            @Override
            public void onServiceDisconnected(ComponentName name) {
 
            }
        };
        // Android5.0中service的intent一定要显式声明,添加服务所在的包名就可将隐式转变为显式
        Intent intent = new Intent();
        intent.setPackage("com.example.remoteservice");
        intent.setAction("com.example.remoteservice.ACTION_PAY");
        bindService(intent, connection, BIND_AUTO_CREATE);
 
        Button btPay = (Button) findViewById(R.id.bt_pay);
        etUser = (EditText) findViewById(R.id.et_username);
        etPassword = (EditText) findViewById(R.id.et_password);
 
        btPay.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    String username = etUser.getText().toString().trim();
                    String password = etPassword.getText().toString().trim();
                  // 账号和密码非空校验
                    if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password)) {
                        Toast.makeText(MainActivity.this, "用户名和密码不能为空!", Toast.LENGTH_SHORT).show();
                    } else {
                      // 账号和密码和远程端的一致才能支付成功
                        boolean hasPayed = myPay.pay(username, password, 1000);
                        if (hasPayed) {
                            Toast.makeText(MainActivity.this, "支付成功!", Toast.LENGTH_SHORT).show();
                        } else {
                          // 密码或者账号输入错误,支付失败
                            Toast.makeText(MainActivity.this, "账号不存在或者密码有误!", Toast.LENGTH_SHORT).show();
                        }
                    }
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        });
    }
 
    @Override
    protected void onDestroy() {
      // 记得解绑服务
        unbindService(connection);
        super.onDestroy();
    }
}
 

输入账号admin,密码123456,和远程端一致,就能付款成功了!

Android中的服务_第1张图片

而且在远程端会在控制台打印:支付X已收到您的付款,共1000元!这个方法确实是跨程序调用了远程端的。

如果账号密码错误就是下图了,不能购买。

Android中的服务_第2张图片

by @sunhaiyu

2017.5.26

你可能感兴趣的:(Android中的服务)