安卓学习笔记(十)服务Service
总结《第一行代码》Android学习笔记(十)服务Service
- 服务(Service)
- Android多线程编程
- 线程基本用法
- 在子线程中更新UI
- 解析异步消息处理机制
- 使用AsyncTask
- 服务的基本用法
- 服务的生命周期
- 服务的更多技巧
- 使用前台服务
- 使用IntentService(解决ANR)
- 服务实践(下载功能)
服务(Service)
Android多线程编程
线程基本用法
(一)继承方法:
class MyThread extents Thread{@Overridepublic void run(){//处理具体逻辑}}//启动线程new MyThread().start();
(二)实现Runnable接口
class MyThread implements Runnable{@Overridepublic void run(){//处理具体逻辑}}//启动线程MyThread myThread = new MyThread();new Thread(myThread).start();
(三)匿名类
new Thread(new Runnable(){@Overridepublic void run(){//处理具体逻辑}}).start();
在子线程中更新UI
因为Android的UI也是线程不完全的,因此如果想要更新应用程序里的UI元素,则必须在主线程中进行,否则就会出现异常。如果想要实现子线程更新UI,需要使用异步消息处理。
public class MainActivity extends AppCompatActivity { public static final int UPDATE_TEXT = 1;//表示更新TextView动作 private TextView text; private Handler handler = new Handler(){ public void handleMessage(Message msg){ switch (msg.what){ case UPDATE_TEXT: //在这里可以进行UI操作 text.setText("Nice to meet you"); break; default: break; } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); text = findViewById(R.id.text); Button changeText = findViewById(R.id.change_text); changeText.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { new Thread(new Runnable() { @Override public void run() { Message message = new Message();//创建Message对象 message.what = UPDATE_TEXT;//设置what字段 handler.sendMessage(message);//将Message对象发送出去 } }).start(); } }); }}
解析异步消息处理机制
Android的异步消息处理主要由4个部分组成:Message、Handler、MessageQueue和Looper。
1.Message
Message是线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。
2.Handler
Handler是处理者,它主要是用于发送和处理消息的。发送消息是使用Handler的sendMessage()方法,发出的消息经过处理后,最终会传递到Handler的handleMessage()方法中。
3.MessageQueue
MessageQueue是消息队列,它主要用于存放所有通过Handler发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程只会有一个MessageQueue对象。
4.Looper
Looper是每个线程中的MessageQueue的管家,调用Looper的loop()方法后就会进入循环,每发现MessageQueue中存在一条消息就会将他取出,并传递到Handler的handleMessage()方法中。每个线程中只有一个Looper对象。
使用AsyncTask
实现进度条:
public class ProgressBarTask extends AppCompatActivity { private static final String TAG = "ProgressBarTask"; private ProgressBar progressBar; private TextView progress; private Button downloadBt; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_progress_bar_task); downloadBt = findViewById(R.id.download_btn); downloadBt.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { downloadBt.setEnabled(false); downloadBt.setText("正在下载"); new MyTask().execute(); } }); } class MyTask extends AsyncTask<Integer, Integer, String > { //后台任务开始执行前调用,用于执行初始化操作 @Override protected void onPreExecute() { LogUtil.d(TAG, "onPreExecute: executed"); progressBar = findViewById(R.id.progress_bar); progress = findViewById(R.id.progress); super.onPreExecute(); } //后台任务,用于执行耗时操作 @Override protected String doInBackground(Integer... integers) {//参数类型为AsyncTask第一个泛型参数,返回值为AsyncTask第三个泛型参数 LogUtil.d(TAG, "doInBackground: executed"); for(int i = 1; i <= 10; i ++){ try{ Thread.sleep(1000);//每秒下载10% publishProgress(10 * i);//调用onProgressUpdate方法,传递数据values } catch (InterruptedException e) { e.printStackTrace(); } } return "下载完成"; } //调用publishProgress()后调用,用于对UI进行操作 @Override protected void onProgressUpdate(Integer... values) {//接收数据values,参数类型为AsyncTask第二个泛型参数 LogUtil.d(TAG, "onProgressUpdate: executed"); super.onProgressUpdate(values); //更新下载进度 progressBar.setProgress(values[0]); progress.setText(values[0] + "%"); } //doInBackground完毕后调用,用于显示结果 @Override protected void onPostExecute(String string) {//接收doInBackground的返回值 LogUtil.d(TAG, "onPostExecute: executed"); super.onPostExecute(string); downloadBt.setText(string); downloadBt.setEnabled(true); } }}
实现计时器:
public class TimerTask extends AppCompatActivity { private static final String TAG = "TimerTask"; private Button timerBTN; private TextView windowTV; private EditText settingET; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_timer_task); timerBTN = findViewById(R.id.timer_btn); settingET = findViewById(R.id.setting_et); timerBTN.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { int time=Integer.parseInt(settingET.getText().toString());//将字符串转换成整形 new MyTask().execute(time); } }); } class MyTask extends AsyncTask<Integer, Integer, String> { //后台任务开始执行前调用,用于执行初始化操作 @Override protected void onPreExecute() { LogUtil.d(TAG, "onPreExecute: executed"); super.onPreExecute(); windowTV = findViewById(R.id.window_tv); } //后台任务,用于执行耗时操作 @Override protected String doInBackground(Integer... integers) { LogUtil.d(TAG, "doInBackground: executed"); for(int i=integers[0]; i >= 0; i--){ try{ Thread.sleep(1000); publishProgress(i);//调用onProgressUpdate方法 } catch (InterruptedException e) { e.printStackTrace(); } } return "计时结束"; } //调用publishProgress()后调用,用于对UI进行操作 @Override protected void onProgressUpdate(Integer... values) { LogUtil.d(TAG, "onProgressUpdate: executed"); super.onProgressUpdate(values); windowTV.setText(values[0] + "" ); } //doInBackground完毕后调用,用于显示结果 @Override protected void onPostExecute(String string) { LogUtil.d(TAG, "onPostExecute: executed"); super.onPostExecute(string); windowTV.setText(string); } }}
服务的基本用法
在项目中使用AS快捷方式创建名为MyService的Service,继承自Service:
public class MyService extends Service { private static final String TAG = "MyService"; private DownloadBinder mBinder = new DownloadBinder(); //使用Binder实现活动和服务间通信 class DownloadBinder extends Binder{ public void startDownload(){ Log.d(TAG, "startDownload: executed"); } public int getProgress(){ Log.d(TAG, "getProgress: executed"); return 0; } } //绑定服务时调用 @Override public IBinder onBind(Intent intent) { Log.d(TAG, "onBind: executed"); return mBinder; } //解绑服务时调用@Override public boolean onUnbind(Intent intent) { Log.d(TAG, "onUnbind: executed"); return super.onUnbind(intent); } public MyService() { } // 服务创建时调用 @Override public void onCreate() { super.onCreate(); Log.d(TAG, "onCreate: executed"); } //服务启动时调用 @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d(TAG, "onStartCommand: executed"); return super.onStartCommand(intent, flags, startId); } //服务销毁时调用 @Override public void onDestroy() { super.onDestroy(); Log.d(TAG, "onDestroy: executed"); }}
因为使用AS快捷方式创建的服务,因此系统自动帮我们在AndroidManifest中注册了服务:
<service android:name=".MyService" android:enabled="true" android:exported="true">service>
启动和停止服务:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{ private MyService.DownloadBinder downloadBinder;//ServiceConnection匿名类 private ServiceConnection connection = new ServiceConnection() {//活动与服务绑定成功时调用 @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { downloadBinder = (MyService.DownloadBinder) iBinder; downloadBinder.startDownload(); downloadBinder.getProgress(); }//活动与服务连接断开时调用 @Override public void onServiceDisconnected(ComponentName componentName) { } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button startService = findViewById(R.id.start_service); Button stopService = findViewById(R.id.stop_service); Button bindService = findViewById(R.id.bind_service); Button unbindService = findViewById(R.id.unbind_service); startService.setOnClickListener(this); stopService.setOnClickListener(this); bindService.setOnClickListener(this); unbindService.setOnClickListener(this); } @Override public void onClick(View view) { switch (view.getId()){ case R.id.start_service: Log.i("MainActivity", "Thread id is " + Thread.currentThread().getId()); Intent startIntent = new Intent(this,MyService.class); startService(startIntent);//启动服务 break; case R.id.stop_service: Intent stopIntent = new Intent(this,MyService.class); stopService(stopIntent);//停止服务 break; case R.id.bind_service: Intent bindIntent = new Intent(this,MyService.class); //绑定服务,参数为Intent对象、ServiceConnection对象和标志位 bindService(bindIntent,connection,BIND_AUTO_CREATE);//绑定服务后自动创建服务 break; case R.id.unbind_service: unbindService(connection);//解绑服务 break; case R.id.start_intent_service: //打印主线程的id Log.i("MainActivity", "Thread id is " + Thread.currentThread().getId()); Intent intentService = new Intent(this,MyIntentService.class); startService(intentService); default: break; } }}
服务的生命周期
服务的生命周期分为startService()和bindService()两种:
如果一个服务既调用startService()方法,有调用bindService()方法的话,想要服务销毁则需要同时调用stopService()和unbindService()方法,onDestroy()方法才会执行。
服务的更多技巧
使用前台服务
在MyService类中修改onCreate方法:
// 服务创建时调用@Overridepublic void onCreate() { super.onCreate(); Log.d(TAG, "onCreate: executed"); //创建前台服务 Intent intent = new Intent(this,MainActivity.class); PendingIntent pi = PendingIntent.getActivity(this,0,intent,0); NotificationCompat.Builder builder = null; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {//如果模拟器SDK版本高于26 NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); NotificationChannel mChannel = new NotificationChannel("q", "aaa", NotificationManager.IMPORTANCE_HIGH); manager.createNotificationChannel(mChannel); builder = new NotificationCompat.Builder(this,"q"); }else{ builder = new NotificationCompat.Builder(this); } builder .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) .setDefaults(NotificationCompat.DEFAULT_ALL) .build(); startForeground(1,builder.getNotification());//让MyService变成前台服务}
使用IntentService(解决ANR)
因为服务中的代码都是默认运行在主线程中,如果直接在服务中处理一些耗时逻辑,就很容易出现ANR(应用程序未响应)的情况。要想解决ANR则需要使用Android多线程编程,但如果在服务的每个具体方法里都开启一个子线程用于处理耗时的逻辑的话,每次都需要在服务执行完毕后调用stopService()或者stopSelf()才能让服务停止下来,这样就会非常繁琐而且很容易忘记停止服务。那么如何才能简单创建一个异步的、会自动停止的服务呢?这时候就可以使用IntentService来实现。
新建MyIntentService类继承自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"); }}
因为MyIntentService是手动创建而不是AS快捷方式创建,因此需要在AndroidManifest中注册服务:
<service android:name=".MyIntentService">service>
修改MainActivity代码:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{ private MyService.DownloadBinder downloadBinder; //ServiceConnection匿名类 private ServiceConnection connection = new ServiceConnection() { //活动与服务绑定成功时调用 @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { downloadBinder = (MyService.DownloadBinder) iBinder; downloadBinder.startDownload(); downloadBinder.getProgress(); } //活动与服务连接断开时调用 @Override public void onServiceDisconnected(ComponentName componentName) { } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button startService = findViewById(R.id.start_service); Button stopService = findViewById(R.id.stop_service); Button bindService = findViewById(R.id.bind_service); Button unbindService = findViewById(R.id.unbind_service); Button startIntentService = findViewById(R.id.start_intent_service); startService.setOnClickListener(this); stopService.setOnClickListener(this); bindService.setOnClickListener(this); unbindService.setOnClickListener(this); startIntentService.setOnClickListener(this); } @Override public void onClick(View view) { switch (view.getId()){ case R.id.start_service: Log.i("MainActivity", "Thread id is " + Thread.currentThread().getId()); Intent startIntent = new Intent(this,MyService.class); startService(startIntent);//启动服务 break; case R.id.stop_service: Intent stopIntent = new Intent(this,MyService.class); stopService(stopIntent);//停止服务 break; case R.id.bind_service: Intent bindIntent = new Intent(this,MyService.class); //绑定服务,参数为Intent对象、ServiceConnection对象和标志位 bindService(bindIntent,connection,BIND_AUTO_CREATE);//绑定服务后自动创建服务 break; case R.id.unbind_service: unbindService(connection);//解绑服务 break; case R.id.start_intent_service: //打印主线程的id Log.i("MainActivity", "Thread id is " + Thread.currentThread().getId()); Intent intentService = new Intent(this,MyIntentService.class); startService(intentService); default: break; } }}
运行后观察logcat可以发现,主线程id和当前线程id不相同,而且在服务结束后自动执行onDestroy。
服务实践(下载功能)
因为需要发送网络请求,因此使用OkHttp实现,添加依赖okhttp:
implementation("com.squareup.okhttp3:okhttp:4.0.1")
定义回调接口,用于对下载过程中的各种状态进行监听和回调:
public interface DownloadListener { void onProgress(int progress);//用于通知当前的下载进度 void onSuccess();//用于通知下载成功事件 void onFailed();//用于通知下载失败事件 void onPaused();//用于通知下载暂停事件 void onCanceled();//用于通知下载取消事件}
使用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; } //在后台执行具体的下载逻辑 @Override protected Integer doInBackground(String... strings) { InputStream is = null; RandomAccessFile savedFile = null; File file = null; try{ long downloadedLength = 0;//记录已下载的文件长度 String downloadUrl = strings[0];//获得下载URL地址 String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));//根据URL地址解析出文件名 String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();//内置SD卡的DOWNLOADS路径 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; } //使用OkHttp发送一条网络请求 OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() //断点下载,指定从哪个字节开始下载 .addHeader("RANGE","bytes=" + downloadedLength + "-") .url(downloadUrl) .build(); Response response = client.newCall(request).execute(); if(response != null){ is = response.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{ total += 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; } //界面上更新当前下载进度 @Override protected void onProgressUpdate(Integer... values) { int progress = values[0];//获取当前下载进度 if(progress > lastProgress){//如果进度有变化 listener.onProgress(progress); lastProgress = progress; } } //用于通知最终的下载结果 @Override protected void onPostExecute(Integer integer) { switch (integer){ case TYPE_SUCCESS: listener.onSuccess(); break; case TYPE_FAILED: listener.onFailed(); break; case TYPE_PAUSED: listener.onPaused(); break; case TYPE_CANCELED: listener.onCanceled(); break; default: break; } } public void pauseDownload(){ isPaused = true; } public void cancelDownload(){ isCanceled = 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; }}
为了保证DownloadTask可以一直在后台运行,我们还需要创建一个下载的服务,使用AS快捷方式创建一个名为DownloadService的服务,继承自Service:
public class DownloadService extends Service { private DownloadTask downloadTask; private String downloadUrl; private DownloadListener listener = new DownloadListener() { @Override public void onProgress(int progress) { getNotificationManager().notify(1,getNotification("Downloading...",progress)); } @Override public void onSuccess() { downloadTask = null; //下载成功时将前台服务通知关闭,并创建一个下载成功的通知 stopForeground(true); getNotificationManager().notify(1,getNotification("Download Success",-1)); Toast.makeText(DownloadService.this,"Download Success",Toast.LENGTH_SHORT).show(); } @Override public void onFailed() { downloadTask = null; //下载失败时将前台服务关闭,并创建一个下载失败的通知 stopForeground(true); getNotificationManager().notify(1,getNotification("Download Failed",-1)); Toast.makeText(DownloadService.this,"Download Failed",Toast.LENGTH_SHORT).show(); } @Override public void onPaused() { downloadTask = null; Toast.makeText(DownloadService.this,"Paused",Toast.LENGTH_SHORT).show(); } @Override public void onCanceled() { downloadTask = null; stopForeground(true); Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_SHORT).show(); } }; private DownloadBinder mBinder = new DownloadBinder(); public DownloadService() { } @Override 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 = downloadUrl.substring(downloadUrl.lastIndexOf("/")); String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath(); File file = new File(directory + fileName); if(file.exists()){ file.delete(); } } } } } 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 builder = null; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { NotificationChannel mChannel = new NotificationChannel("youyu4", "aaa", NotificationManager.IMPORTANCE_DEFAULT); getNotificationManager().createNotificationChannel(mChannel); builder = new NotificationCompat.Builder(this,"youyu4"); }else{ builder = new NotificationCompat.Builder(this); } builder.setSmallIcon(R.mipmap.ic_launcher); builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher)); builder.setContentIntent(pi); builder.setContentTitle(title); if(progress >= 0){ //当progress大于等于0时才需显示下载进度 builder.setContentText(progress + "%"); builder.setProgress(100,progress,false);//设置进度条的最大进度、当前进度和是否使用模糊进度条 } return builder.build(); }}
在布局中添加三个按钮用于开始下载、暂停下载和取消下载,修改MainActivity:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{ private DownloadService.DownloadBinder downloadBinder; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { downloadBinder = (DownloadService.DownloadBinder) iBinder;//获取DownloadBinder实例 } @Override public void onServiceDisconnected(ComponentName componentName) { } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button startDownload = findViewById(R.id.start_download); Button pauseDownload = findViewById(R.id.pause_download); Button cancelDownload = findViewById(R.id.cancel_download); startDownload.setOnClickListener(this); pauseDownload.setOnClickListener(this); cancelDownload.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); } } @Override public void onClick(View view) { switch (view.getId()){ case R.id.start_download: String url = "http://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; } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode){ case 1: if(grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED){ Toast.makeText(this,"拒绝权限将无法使用程序",Toast.LENGTH_SHORT).show(); finish(); } break; default: } } @Override protected void onDestroy() { super.onDestroy(); unbindService(connection); }}
最后再AndroidManifest声明使用到的权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/><uses-permission android:name="android.permission.INTERNET"/><uses-permission android:name="android.permission.FOREGROUND_SERVICE"/><uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
更多相关文章
- SpringBoot 2.0 中 HikariCP 数据库连接池原理解析
- tcping测试服务器TCP端口
- android的GCM研究
- Android之Handler有感(二)
- android学习笔记——Handler
- Android(安卓)EventBus实战 没听过你就out了
- window 安装android
- 源码下载:74个Android开发开源项目汇总
- Handler消息传送机制