14、服务
服务(Service)是 Android 中实现程序后台运行的解决方案
它非常适合用于去执行那些不需要和用户交互而且还要求长期运行的任务。
服务并不会自动开启线程,所有的代码都是默认运行在主线程当中的。也就是说,我们需要在服务的内部手动创建子线程,并在这里执行具体的任务,否则就有可能出现主线程被阻塞住的情况。
多线程编程:
Android 确实是不允许在子线程中进行 UI 操作的。但是有些时候,我们必须在子线程里去执行一些耗时任务,然后根据任务的执行结果来更新相应的 UI 控件,这该如何是好呢?
对于这种情况,Android 提供了一套异步消息处理机制,完美地解决了在子线程中进行UI 操作的问题
//在线程中调用 Handler 的 sendMessage()方法将这条Message 发送出去,Handler 就会收到这条 Message,并在 handleMessage()方法中对它进行处理。注意此时 handleMessage()方法中的代码就是在主线程当中运行的了,所以我们可以放心地在这里进行 UI 操作。
private Handler handler = new Handler() { public void handleMessage(Message msg) { switch (msg.what) { case 1: // 在这里可以进行UI操作 changeText.setText("Nice to meet you"); break; default: break; } } }; changeTextBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(MainActivity.this, "Test", Toast.LENGTH_SHORT).show(); //注意线程最后需要启动 new Thread(new Runnable() { @Override public void run() { //changeText.setText("Nice to meet you"); Message message = new Message(); message.what = 1; handler.sendMessage(message); // 将Message对象发送出去 } }).start(); } });
Android 中的异步消息处理主要由四个部分组成,
Message、Handler、MessageQueue 和Looper
1. Message
Message 是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。
上一小节中我们使用到了 Message 的 what 字段,除此之外还可以使用 arg1 和 arg2 字段来携带一些整型数据,使用 obj 字段携带一个 Object 对象。
2. Handler
Handler 顾名思义也就是处理者的意思,它主要是用于发送和处理消息的。
发送消息一般是使用 Handler 的 sendMessage()方法,而发出的消息经过一系列地辗转处理后,最终会传递到 Handler 的 handleMessage()方法中。
3. MessageQueue
MessageQueue 是消息队列的意思,它主要用于存放所有通过 Handler 发送的消息。
这部分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个 MessageQueue对象。
4. Looper
Looper 是每个线程中的 MessageQueue 的管家,调用 Looper 的 loop()方法后,就会进入到一个无限循环当中,然后每当发现 MessageQueue 中存在一条消息,就会将它取
出,并传递到 Handler 的 handleMessage()方法中。每个线程中也只会有一个 Looper 对象。
异步消息处理流程:
首先需要在主线程当中创建一个 Handler 对象,并重写handleMessage()方法。然后当子线程中需要进行 UI 操作时,就创建一个 Message 对象,并通过 Handler 将这条消息发送出去。【之后这条消息会被添加到 MessageQueue 的队列中等待被处理,而 Looper 则会一直尝试从 MessageQueue 中取出待处理消息,最后分发回 Handler的 handleMessage()方法中】。由于 Handler 是在主线程中创建的,所以此时 handleMessage()方法中的代码也会在主线程中运行,于是我们在这里就可以安心地进行 UI 操作了。
使用 AsyncTask;
由于 AsyncTask 是一个抽象类,所以如果我们想使用它,就必须要继承它。在继承时我们可以为 AsyncTask 类指定三个泛型参数,这三个参数的用途如下。
1. Params
在执行 AsyncTask 时需要传入的参数,可用于在后台任务中使用。
2. Progress
后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度
单位。
3. Result
当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。
因此,一个最简单的自定义 AsyncTask 就可以写成如下方式:
class DownloadTask extends AsyncTask<Void, Integer, Boolean> { …… }
1. onPreExecute()
这个方法会在后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。
2. doInBackground(Params...)
这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。任务一旦完成就可以通过 return 语句来将任务的执行结果返回,如果 AsyncTask 的
第三个泛型参数指定的是 Void,就可以不返回任务执行结果。注意,在这个方法中是不可以进行 UI 操作的,如果需要更新 UI 元素,比如说反馈当前任务的执行进度,可以调用 publishProgress(Progress...)方法来完成。
3. onProgressUpdate(Progress...)
当在后台任务中调用了 publishProgress(Progress...)方法后,这个方法就会很快被调用,方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对 UI 进行操作,利用参数中的数值就可以对界面元素进行相应地更新。
4. onPostExecute(Result)
当后台任务执行完毕并通过 return 语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据来进行一些 UI 操作,比如说提醒任务执行的结果,以及关闭掉进度条对话框等。
//启动任务: new DownloadTask().execute() //下载任务: class DownloadTask extends AsyncTask<Void, Integer, Boolean> { protected void onPreExecute() { progressDialog.show(); // 显示进度对话框 } @Override protected Boolean doInBackground(Void... params) { try { while (true) { int downloadPercent = doDownload(); // 这是一个虚构的方法 publishProgress(downloadPercent); if (downloadPercent >= 100) { break; } } } catch (Exception e) { return false; } return true; } @Override protected void onProgressUpdate(Integer... values) { // 在这里更新下载进度 progressDialog.setMessage("Downloaded " + values[0] + "%"); } @Override protected void onPostExecute(Boolean result) { progressDialog.dismiss(); // 关闭进度对话框 // 在这里提示下载结果 if (result) { Toast.makeText(context, "Download succeeded",Toast.LENGTH_SHORT) .show(); } else { Toast.makeText(context, " Download failed", Toast.LENGTH_SHORT).show(); } } }
服务:
每一个服务都需要在 AndroidManifest.xml文件中进行注册才能生效
<application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > …… <service android:name=".MyService" ></service> </application>
public class MyService extends Service { @Override public IBinder onBind(Intent intent) { return null; } //onCreate()方法会在服务创建的时候调用 //onCreate()方法是在服务第一次创建的时候调用的,而 onStartCommand()方法则在 每次启动服务的时候都会调用 @Override public void onCreate() { super.onCreate(); } //onStartCommand()方法会在每次服务启动的时候调用 @Override public int onStartCommand(Intent intent, int flags, int startId) { return super.onStartCommand(intent, flags, startId); } //onDestroy()方法会在服务销毁的时候调用 @Override public void onDestroy() { super.onDestroy(); } }
启动服务:
Intent startIntent = new Intent(this, MyService.class);
startService(startIntent); // 启动服务
停止服务:
Intent startIntent = new Intent(this, MyService.class);
stopService(startIntent); // 启动服务
活动和服务进行通信
当一个活动和服务绑定了之后,就可以调用该服务里的 Binder 提供的方法了
首先创建了一个 ServiceConnection 的匿名类,在里面重写了onServiceConnected()方法和 onServiceDisconnected()方法,这两个方法分别会在活动与服务成功绑定以及解除绑定的时候调用。在 onServiceConnected()方法中,我们又通过向下转型得到了 DownloadBinder 的实例,有了这个实例,活动和服务之间的关系就变得非常紧密了
Activity:
private MyService.DownloadBinder downloadBinder; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName name) { } @Override public void onServiceConnected(ComponentName name, IBinder service) { downloadBinder = (MyService.DownloadBinder) service; downloadBinder.startDownload(); downloadBinder.getProgress(); } }; //绑定服务和取消绑定服务按钮事件 case R.id.bind_service: //绑定服务 /*bindService()方法接收三个参数, *第一个参数就是刚刚构建出的 Intent 对象,第二个参数是前面创建出的 ServiceConnection 的实例,第三个参数则是一个标志位,这里传入 BIND_AUTO_CREATE *表示在活动和 服务进行绑定后自动创建服务。这会使得 MyService 中的 onCreate()方法得到执行,但 onStartCommand()方法不会执行。 */ Intent bindIntent = new Intent(this, MyService.class); bindService(bindIntent, connection, BIND_AUTO_CREATE); // 绑定服务 break; case R.id.unbind_service: unbindService(connection); // 解绑服务 break;
MyService中:
public class MyService extends Service { public MyService() { } private DownloadBinder mBinder = new DownloadBinder(); //服务内部类 class DownloadBinder extends Binder { public void startDownload() { Log.d("MyService", "startDownload executed"); Toast.makeText(MyService.this, "startDownload executed", Toast.LENGTH_SHORT).show(); } public int getProgress() { Log.d("MyService", "getProgress executed"); Toast.makeText(MyService.this, "getProgress executed", Toast.LENGTH_SHORT).show(); return 0; } } @Override public IBinder onBind(Intent intent) { //返回内部类对象 return mBinder; } @Override public void onCreate() { super.onCreate(); Toast.makeText(this, "MyService onCreate!", Toast.LENGTH_SHORT).show(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Toast.makeText(this, "MyService start !", Toast.LENGTH_SHORT).show(); return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { super.onDestroy(); Toast.makeText(this, "MyService onDestory !", Toast.LENGTH_SHORT).show(); } }
服务生命周期:
一旦在项目的任何位置调用了 Context 的 startService()方法,相应的服务就会启动起来,并回调 onStartCommand()方法。【如果这个服务之前还没有创建过,onCreate()方法会先于onStartCommand()方法执行】。服务启动了之后会一直保持运行状态,直到 stopService()或stopSelf()方法被调用。注意虽然每调用一次 startService()方法,onStartCommand()就会执行一次,【但实际上每个服务都只会存在一个实例。所以不管你调用了多少次 startService()方法,只需调用一次 stopService()或 stopSelf()方法,服务就会停止下来了】。
还可以调用 Context 的 bindService()来获取一个服务的持久连接,这时就会回调服务中的 onBind()方法。类似地,如果这个服务之前还没有创建过,onCreate()方法会先于onBind()方法执行。
只要调用方和服务之间的连接没有断开,服务就会一直保持运行状态
当调用了 startService()方法后,又去调用 stopService()方法,这时服务中的 onDestroy()方法就会执行,表示服务已经销毁了。
类似地,当调用了 bindService()方法后,又去调用unbindService()方法,onDestroy()方法也会执行
一个服务既调用了 startService()方法,又调用了 bindService()方法,这种情况下要同时调用 stopService()和 unbindService()方法,onDestroy()方法才会执行
前台服务:
如果你希望服务可以一直保持运行状态,而不会由于系统内存不足的原因导致被回收,就可以考虑使用前台服务。
前台服务和普通服务最大的区别就在于,它会一直有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果。
在服务的onCreate()方法中:
Notification.Builder builder = new Notification.Builder(this); builder.setTicker("这是一个通知222") .setContentTitle("这是TITLE2222") .setContentText("这是2222通知的内容这是通知的内容这是通知的内容这是 通知的内容这是通知的内容这是通知的内容") .setWhen(System.currentTimeMillis()) .setAutoCancel(false) .setSmallIcon(R.drawable.pineapple_pic); Notification notification = builder.getNotification(); Intent intent = new Intent(this, SecondActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0); builder.setContentIntent(pendingIntent); startForeground(1,notification);
IntentService
服务中的代码都是默认运行在主线程当中的,如果直接在服务里去处理一些耗时的逻辑,就很容易出现 ANR(Application NotResponding)的情况。
耗时服务写法1:
public int onStartCommand(Intent intent, int flags, int startId) { //开启线程 new Thread(new Runnable() { @Override public void run() { // 处理具体的逻辑 // 执行完毕后自动停止 stopSelf(); } }).start(); return super.onStartCommand(intent, flags, startId); }
异步的、会自动停止的服务IntentService:
public class MyIntentService extends IntentService { public MyIntentService() { super("MyIntentService"); // 调用父类的有参构造函数 } @Override protected void onHandleIntent(Intent intent) { // 打印当前线程的id Log.d("MyIntentService", "Thread id is " + Thread.currentThread(). getId()); //在这个方法中可以去处理一些具体的逻辑 } @Override public void onDestroy() { super.onDestroy(); Log.d("MyIntentService", "onDestroy executed"); } }
后台执行定制任务:
Android 中的定时任务一般有两种实现方式,
一种是使用 Java API 里提供的 Timer 类,一种是使用 Android 的 Alarm 机制。
但 Timer有一个明显的短板,它并不太适用于那些需要长期在后台运行的定时任务。Android 手机就会在长时间不操作的情况下自动让 CPU 进入到睡眠状态,这就有可能导致 Timer 中的定时任务无法正常运行。
而 Alarm 机制则不存在这种情况,它具有唤醒 CPU 的功能,即可以保证每次需要执行定时任务的时候 CPU 都能正常工作。
//设定一个任务在 10 秒钟后执行,就可以写成:
AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); long triggerAtTime = SystemClock.elapsedRealtime() + 10 * 1000; manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pendingIntent);
例子:
构造一个后台每隔1个小时执行一次的定时任务
//MainActivity中启动服务: Intent intent = new Intent(this, LongRunningService.class); startService(intent); //新建服务LongRunningService: public class LongRunningService extends Service { @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { new Thread(new Runnable() { @Override public void run() { Log.d("LongRunningService", "executed at " + new Date(). toString()); } }).start(); AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE); int anHour = 60 * 60 * 1000; // 这是一小时的毫秒数 long triggerAtTime = SystemClock.elapsedRealtime() + anHour; //广播接收器AlarmReceiver Intent i = new Intent(this, AlarmReceiver.class); PendingIntent pi = PendingIntent.getBroadcast(this, 0, i, 0); manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pi); return super.onStartCommand(intent, flags, startId); } } //构造广播接收器: public class AlarmReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Intent i = new Intent(context, LongRunningService.class); context.startService(i); } }
配置文件中注册:
<service android:name=".LongRunningService" ></service> <receiver android:name=".AlarmReceiver" ></receiver>
基于位置:
授权:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
public class LocationActivity extends BaseActivity { private TextView positionTextView; private LocationManager locationManager; private String provider; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.location_layout); //无法显示位置,6.0之后处理方法见后面 setLocation(); } public void setLocation(){ positionTextView = (TextView) findViewById(R.id.position_text_view); locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); // 获取所有可用的位置提供器 List<String> providerList = locationManager.getProviders(true); if (providerList.contains(LocationManager.GPS_PROVIDER)) { provider = LocationManager.GPS_PROVIDER; } else if (providerList.contains(LocationManager.NETWORK_PROVIDER)) { provider = LocationManager.NETWORK_PROVIDER; } else { // 当没有可用的位置提供器时,弹出Toast提示用户 Toast.makeText(this, "No location provider to use",Toast.LENGTH_SHORT).show(); return; } Location location = locationManager.getLastKnownLocation(provider); if (location != null) { // 显示当前设备的位置信息 showLocation(location); } locationManager.requestLocationUpdates(provider, 5000, 1,locationListener); } protected void onDestroy() { super.onDestroy(); if (locationManager != null) { // 关闭程序时将监听器移除 locationManager.removeUpdates(locationListener); } } LocationListener locationListener = new LocationListener() { @Override public void onStatusChanged(String provider, int status, Bundle extras) { } @Override public void onProviderEnabled(String provider) { } @Override public void onProviderDisabled(String provider) { } @Override public void onLocationChanged(Location location) { // 更新当前设备的位置信息 showLocation(location); } }; private void showLocation(Location location) { String currentPosition = "latitude is " + location.getLatitude() + "\n" + "longitude is " + location.getLongitude(); positionTextView.setText(currentPosition); } }
6.0 需要在应用运行过程中请求位置权限,并且还要打开位置。
Android 6.0 扫描不到 Ble 设备需开启位置权限
https://blog.csdn.net/kjunchen/article/details/52769915
6.0 需要在应用运行过程中请求位置权限,并且还要打开位置
添加授权:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
//判断权限: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {//如果 API level 是大于等于 23(Android 6.0) 时 //判断是否具有权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { //判断是否需要向用户解释为什么需要申请该权限 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_COARSE_LOCATION)) { Toast.makeText(this, "自Android 6.0开始需要打开位置权限才可以搜索到Ble设备", Toast.LENGTH_SHORT).show(); } //请求权限 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 1); } }else{ setLocation(); } //回调 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == 1) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { //用户允许改权限,0表示允许,-1表示拒绝 PERMISSION_GRANTED = 0, PERMISSION_DENIED = -1 //permission was granted, yay! Do the contacts-related task you need to do. //这里进行授权被允许的处理 setLocation(); } else { //permission denied, boo! Disable the functionality that depends on this permission. //这里进行权限被拒绝的处理 Toast.makeText(this, "permission denied", Toast.LENGTH_SHORT).show(); } } else { super.onRequestPermissionsResult(requestCode, permissions, grantResults); } }
经纬值转换成看得懂的位置信息
private void showLocation(final Location location) { new Thread(new Runnable() { @Override public void run() { try { // 组装反向地理编码的接口地址 StringBuilder url = new StringBuilder(); url.append("http://maps.googleapis.com/maps/api/geocode/json?latlng="); url.append(location.getLatitude()).append(",") url.append(location.getLongitude()); url.append("&sensor=false"); HttpClient httpClient = new DefaultHttpClient(); HttpGet httpGet = new HttpGet(url.toString()); // 在请求消息头中指定语言,保证服务器会返回中文数据 httpGet.addHeader("Accept-Language", "zh-CN"); HttpResponse httpResponse = httpClient.execute(httpGet); if (httpResponse.getStatusLine().getStatusCode() == 200) { HttpEntity entity = httpResponse.getEntity(); String response = EntityUtils.toString(entity,"utf-8"); JSONObject jsonObject = new JSONObject(response); JSONArray resultArray = jsonObject.getJSONArray("results"); if (resultArray.length() > 0) { JSONObject subObject = resultArray.getJSONObject(0); // 取出格式化后的位置信息 String address = subObject.getString("formatted_address"); Message message = new Message(); message.what = SHOW_LOCATION; message.obj = address; handler.sendMessage(message); } } } catch (Exception e) { e.printStackTrace(); } } }).start(); } private Handler handler = new Handler() { public void handleMessage(Message msg) { switch (msg.what) { case SHOW_LOCATION: String currentPosition = (String) msg.obj; positionTextView.setText(currentPosition); break; default: break; } } };
...