目录

前言

方式一:AlarmManager

API19之前AlarmManager常用的一些方法

参数说明

使用举例

AlarmManager实例Demo讲解(包含版本适配以及高版本设置重复闹钟)

AlarmManager总结

方式二:Handler实现方式

采用Handle与线程的sleep(long)方法

采用Handler的postDelayed(Runnable, long)方法

采用Handler与timer及TimerTask结合的方法

方式三:ScheduledExecutorService

延迟不循环任务schedule方法

方式三:RXjava实现

方式四:WorkManager实现定时任务

总结


前言

Android开发当中,定时器的场景太多太多,比如过多久轮询一次业务需求,或者轮询网络以及多少秒的倒计时,记录一下给需要的人一些帮助

Android中的定时任务一般有两种实现方式,一种是使用 Java API 里提供的 Timer 类,一种是使用 Android 的 Alarm 机制。这两种方式在多数情况下都能实现类似的效果,但 Timer 有一个明显的短板,它并不适用于那些需要长期在后台运行的定时任务。我们都知道,为了能让电池更加耐用,每种手机都会有自己的休眠策略,Android 手机就会在长时间不操作的情况下自动让 CPU 进入到睡眠状态,这就有可能导致 Timer 中的定时任务无法正常运行。而 Alarm 则具有唤醒 CPU 的功能,它可以在需要执行定时任务的时候大吼一声:“小UU,不要跟我 bbll ,赶紧给我起来干活,不然你看我扎不扎你就完了。” 需要注意,这里唤醒 CPU 和唤醒屏幕完全不是一个概念,千万不要混淆。

方式一:AlarmManager

API19之前AlarmManager常用的一些方法

  • set(int type,long startTime,PendingIntent pi) //该方法用于设置一次性定时器,到达时间执行完就GG了
  • setRepeating(int type,long startTime,long intervalTime,PendingIntent pi)//该方法用于设置可重复执行的定时器
  • setInexactRepeating(int type,long startTime,long intervalTime,PendingIntent pi)//该方法用于设置可重复执行的定时器。与setRepeating相比,这个方法更加考虑系统电量,比如系统在低电量情况下可能不会严格按照设定的间隔时间执行闹钟,因为系统可以调整报警的交付时间,使其同时触发,避免超过必要的唤醒设备。

参数说明

int type:闹钟类型,常用有几个类型,说明如下: | | | |--|--| | AlarmManager.ELAPSED_REALTIME |表示闹钟在手机睡眠状态下不可用,就是睡眠状态下不具备唤醒CPU的能力(跟普通Timer差不多了),该状态下闹钟使用相对时间,相对于系统启动开始。 | |AlarmManager.ELAPSED_REALTIME_WAKEUP|表示闹钟在睡眠状态下会唤醒系统并执行提示功能,该状态下闹钟也使用相对时间| |AlarmManager.RTC|表示闹钟在睡眠状态下不可用,该状态下闹钟使用绝对时间,即当前系统时间| |AlarmManager.RTC_WAKEUP|表示闹钟在睡眠状态下会唤醒系统并执行提示功能,该状态下闹钟使用绝对时间|

long startTime: 定时任务的出发时间,以毫秒为单位。

PendingIntent pi: 到时间后的执行意图。PendingIntent是Intent的封装类。需要注意的是,如果是通过启动服务来实现闹钟提 示的话,PendingIntent对象的获取就应该采用Pending.getService(Context c,int i,Intent intent,int j)方法;如果是通过广播来实现闹钟提示的话,PendingIntent对象的获取就应该采用 PendingIntent.getBroadcast(Context c,int i,Intent intent,int j)方法;如果是采用Activity的方式来实现闹钟提示的话,PendingIntent对象的获取就应该采用 PendingIntent.getActivity(Context c,int i,Intent intent,int j)方法。关于PendingInten不是本文重点,请自行查阅使用方法。

使用举例

需求:定义一个在CPU休眠情况下也能执行的闹钟,到==指定时间==发送一次广播,代码如下:

AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);Calendar calendar = Calendar.getInstance();calendar.set(Calendar.HOUR_OF_DAY,21);calendar.set(Calendar.MINUTE,14);calendar.set(Calendar.SECOND,00);//这里代表 21.14.00Intent intent = new Intent("Li_ALI");  intent.putExtra("msg","阿力起床了啊");     PendingIntent pi = PendingIntent.getBroadcast(this,0,intent,0);   // 到了 21点14分00秒 后通过PendingIntent pi对象发送广播  am.set(AlarmManager.RTC_WAKEUP,calendar.getTimeInMillis(),pi);

AlarmManager实例Demo讲解(包含版本适配以及高版本设置重复闹钟)

好了经过上面讲解,我相信你是似懂非懂的,因为没看到具体代码啊,简单,一个小Demo就全都明白了。 实现功能:在CPU休眠情况下依然可以设定时间启动一次服务,在服务中执行相应逻辑(Demo中只是打印Log),适配各个版本。

先看一下最核心的AlarmManagerUtils类(AlarmManager工具类):

package com.shanya.testalarm;import android.annotation.SuppressLint;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;import java.util.Calendar;public class AlarmManagerUtils {private static final long TIME_INTERVAL = 10 * 1000;//闹钟执行任务的时间间隔private Context context;public static AlarmManager am;public static PendingIntent pendingIntent;private Calendar calendar;//private AlarmManagerUtils(Context aContext) {this.context = aContext;}//singletonprivate static AlarmManagerUtils instance = null;public static AlarmManagerUtils getInstance(Context aContext) {if (instance == null) {synchronized (AlarmManagerUtils.class) {if (instance == null) {instance = new AlarmManagerUtils(aContext);}}}return instance;}public void createGetUpAlarmManager() {am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);Intent intent = new Intent(context, MyService.class);pendingIntent = PendingIntent.getService(context, 0, intent, 0);//每隔10秒启动一次服务}@SuppressLint("NewApi")public void getUpAlarmManagerStartWork() {calendar = Calendar.getInstance();calendar.set(Calendar.HOUR_OF_DAY,23);calendar.set(Calendar.MINUTE,50);calendar.set(Calendar.SECOND,00);//版本适配 System.currentTimeMillis()if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {// 6.0及以上am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,calendar.getTimeInMillis(), pendingIntent);} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {// 4.4及以上am.setExact(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),pendingIntent);} else {am.setRepeating(AlarmManager.RTC_WAKEUP,calendar.getTimeInMillis(), TIME_INTERVAL, pendingIntent);}}@SuppressLint("NewApi")public void getUpAlarmManagerWorkOnOthers() {//高版本重复设置闹钟达到低版本中setRepeating相同效果if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {// 6.0及以上am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,System.currentTimeMillis() + TIME_INTERVAL, pendingIntent);} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {// 4.4及以上am.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()+ TIME_INTERVAL, pendingIntent);}}
}

AlarmManagerUtils就是将与AlarmManager有关的操作都封装起来了,方便解耦。很简单,主要就是版本适配了,上面已经讲解够仔细了,这里就是判断不同版本调用不同API了。

MainActivity代码:

package com.shanya.testalarm;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;public class MainActivity extends AppCompatActivity {private AlarmManagerUtils alarmManagerUtils;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);alarmManagerUtils = AlarmManagerUtils.getInstance(this);alarmManagerUtils.createGetUpAlarmManager();Button button = findViewById(R.id.am);button.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {alarmManagerUtils.getUpAlarmManagerStartWork();Toast.makeText(getApplicationContext(),"设置成功",Toast.LENGTH_SHORT).show();}});}
}

MainActivity中就是调用AlarmManagerUtils中已经封装好的代码进行初始化以及点击Button的时候调用getUpAlarmManagerStartWork方法完成第一次触发AlarmManager。

最后看下服务类中具体做了什么。 MyService类:

package com.shanya.testalarm;import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.os.SystemClock;
import android.util.Log;
import android.widget.Toast;import androidx.annotation.RequiresApi;public class MyService extends Service {private static final String TAG = "MyService";public MyService() {}@Overridepublic IBinder onBind(Intent intent) {// TODO: Return the communication channel to the service.throw new UnsupportedOperationException("Not yet implemented");}@RequiresApi(api = Build.VERSION_CODES.M)@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {new Thread(new Runnable() {@Overridepublic void run() {Log.d(TAG, "run: ");}}).start();AlarmManagerUtils.getInstance(getApplicationContext()).getUpAlarmManagerWorkOnOthers();return super.onStartCommand(intent, flags, startId);}
}

AlarmManager总结

好了,本文到此就该结束了,相信经过以上讲述你对AlarmManager有了更进一步全面了解,在我们使用的时候请不要滥用,考虑一下用户电量,尽量优化自己APP。

方式二:Handler实现方式

采用Handle与线程的sleep(long)方法

Handler主要用来处理接受到的消息。这只是最主要的方法,当然Handler里还有其他的方法供实现,有兴趣的可以去查API,这里不过多解释。
 1. 定义一个Handler类,用于处理接受到的Message。

Handler handler = new Handler(new Handler.Callback() {@Overridepublic boolean handleMessage(@NonNull Message msg) {// 要做的事情  return false;}});

2. 新建一个实现Runnable接口的线程类,如下:


public class MyThread implements Runnable {  @Override  public void run() {  // TODO Auto-generated method stub  while (true) {  try {  Thread.sleep(10000);// 线程暂停10秒,单位毫秒  Message message = new Message();  message.what = 1;  handler.sendMessage(message);// 发送消息  } catch (InterruptedException e) {  // TODO Auto-generated catch block  e.printStackTrace();  }  }  }
}  

3. 在需要启动线程的地方加入下面语句:

new Thread(new MyThread()).start();

4. 启动线程后,线程每10s发送一次消息。

5.最后记得在onDestroy回收清空

采用Handler的postDelayed(Runnable, long)方法

1. 定义一个Handler类

Handler handler=new Handler();
Runnable runnable=new Runnable() {  @Override  public void run() {  // TODO Auto-generated method stub  //要做的事情  handler.postDelayed(this, 2000);  }
};

2. 启动计时器

handler.postDelayed(runnable, 2000);//每两秒执行一次runnable.

3. 停止计时器

handler.removeCallbacks(runnable);

采用Handler与timer及TimerTask结合的方法

1. 定义定时器、定时器任务及Handler句柄

private final Timer timer = new Timer();
private TimerTask task;
Handler handler = new Handler() {  @Override  public void handleMessage(Message msg) {  // TODO Auto-generated method stub  // 要做的事情  super.handleMessage(msg);  }
}; 

2. 初始化计时器任务

task = new TimerTask() {  @Override  public void run() {  // TODO Auto-generated method stub  Message message = new Message();  message.what = 1;  handler.sendMessage(message);  }
};   

3. 启动定时器

timer.schedule(task, 2000, 2000);  

4. 停止计时器

timer.cancel();  

方式三:ScheduledExecutorService

介绍:

ScheduledExecutorService有线程池的特性,也可以实现任务循环执行,可以看作是一个简单地定时任务组件,因为有线程池特性,所以任务之间可以多线程并发执行,互不影响,当任务来的时候,才会真正创建线程去执行
我们在做一些普通定时循环任务时可以用它,比如定时刷新字典常量,只需要不断重复执行即可,这篇文章讲解一下它的用法以及注意事项,不涉及底层原理

注意:我们都知道,在使用线程池的时候,如果我们的任务出现异常没有捕获,那么线程会销毁被回收,不会影响其他任务继续提交并执行,但是在这里,如果你的任务出现异常没有捕获,会导致后续的任务不再执行,所以一定要try...catch

延迟不循环任务schedule方法

schedule(Runnable command, long delay, TimeUnit unit)
参数1:任务
参数2:方法第一次执行的延迟时间
参数3:延迟单位
说明:延迟任务,只执行一次(不会再次执行),参数2为延迟时间

案例说明:

private ScheduledExecutorService mScheduledExecutorService;mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor();mScheduledExecutorService.scheduleAtFixedRate(() -> {try {//有网络的情况下合成播放if (ConfigApp.isExtranet()) {syntheticConsumption();}} catch (Exception e) {Timber.i("合成mScheduledExecutorService出错了!");}}, 0, 1, TimeUnit.SECONDS);

方式三:RXjava实现

rxjava实现方式需要导包,根据需要导入对应的包即可

    //rxjava2api 'io.reactivex.rxjava2:rxjava:2.2.19'api 'io.reactivex.rxjava2:rxandroid:2.1.1'api 'com.jakewharton.rxbinding2:rxbinding:2.2.0'

工具类封装:

package com.maxvision.fyj.common.utils;import android.content.Context;import androidx.annotation.NonNull;import com.maxvision.fyj.common.aop.CheckNetAspect;import org.jetbrains.annotations.NotNull;import java.util.concurrent.TimeUnit;import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;/*** name:cl* date:2022/8/16* desc:*/
public class RxTimer {private Disposable mDisposable;private Disposable mDisposable2;/*** milliseconds毫秒后执行指定动作** @param milliSeconds* @param rxAction*/public void timer(long milliSeconds, final RxAction rxAction) {Observable.timer(milliSeconds, TimeUnit.MILLISECONDS).observeOn(AndroidSchedulers.mainThread()).subscribe(new Observer<Long>() {@Overridepublic void onSubscribe(@NonNull Disposable disposable) {mDisposable = disposable;}@Overridepublic void onNext(@NonNull Long number) {if (rxAction != null) {rxAction.action(number);}}@Overridepublic void onError(@NonNull Throwable e) {//取消订阅cancel();}@Overridepublic void onComplete() {//取消订阅cancel();}});}/*** 每隔milliseconds毫秒后执行指定动作** @param milliSeconds* @param rxAction*/public void interval(long milliSeconds, final RxAction rxAction) {Observable.interval(milliSeconds, TimeUnit.MILLISECONDS).observeOn(AndroidSchedulers.mainThread()).subscribe(new Observer<Long>() {@Overridepublic void onSubscribe(@NonNull Disposable disposable) {mDisposable = disposable;}@Overridepublic void onNext(@NonNull Long number) {if (rxAction != null) {rxAction.action(number);}}@Overridepublic void onError(@NonNull Throwable e) {}@Overridepublic void onComplete() {}});}/*** 网络判断*/public void networkCallback(Context context, RxNetwork rxNetwork) {Observable.just(CheckNetAspect.pingFactory(context)).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Observer<Boolean>() {@Overridepublic void onSubscribe(@NotNull Disposable d) {mDisposable2 = d;}@Overridepublic void onNext(@NotNull Boolean aBoolean) {rxNetwork.networkCB(aBoolean);}@Overridepublic void onError(@NotNull Throwable e) {rxNetwork.networkCB(false);}@Overridepublic void onComplete() {}});}/*** 取消订阅*/public void cancel() {if (mDisposable != null && !mDisposable.isDisposed()) {mDisposable.dispose();}if (mDisposable2 != null && !mDisposable2.isDisposed()) {mDisposable2.dispose();}}public interface RxAction {/*** 让调用者指定指定动作** @param number 时间*/void action(long number);}public interface RxNetwork {void networkCB(boolean network);}
}

使用方法:

        rxTimer = new RxTimer();rxTimer.interval(5000, number -> {LogUtils.e("MQTT","startMqtt===========");      });//取消rxTimer.cancel();

方式四:WorkManager实现定时任务

同样的如果不清楚WorkManager的基础使用,推荐大家看看教程

Android架构组件WorkManager详解

WorkManager的使用相对来说也比较简单, WorkManager组件库里面提供了一个专门做周期性任务的类PeriodicWorkRequest。但是PeriodicWorkRequest类有一个限制条件最小的周期时间是15分钟。

WorkManager 比较适合一些比较长时间的任务。还能设置一些约束条件,比如我们每24小时,在设备充电的时候我们就上传这一整天的Log文件到服务器,比如我们每隔12小时就检查应用是否需要更新,如果需要更新则自动下载安装(需要指定Root设备)。

场景如下,还是那个放在公司前台常亮并且一直运行在前台的平板,我们每12小时就检查自动更新,并自动安装,由于之前写了 AlarmManager 所以安装成功之后App会自动打开。

Data inputData2 = new Data.Builder().putString("version", "1.0.0").build();PeriodicWorkRequest checkVersionRequest =new PeriodicWorkRequest.Builder(CheckVersionWork.class, 12, TimeUnit.HOURS).setInputData(inputData2).build();WorkManager.getInstance().enqueue(checkVersionRequest);WorkManager.getInstance().getWorkInfoByIdLiveData(checkVersionRequest.getId()).observe(this, workInfo -> {assert workInfo != null;WorkInfo.State state = workInfo.getState();Data data = workInfo.getOutputData();String url = data.getString("download_url", "");//去下载并静默安装Apk    downLoadingApkInstall(url)});
/*** 间隔12个小时的定时任务,检测版本的更新*/
public class CheckVersionWork extends Worker {public CheckVersionWork(@NonNull Context context, @NonNull WorkerParameters workerParams) {super(context, workerParams);}@Overridepublic void onStopped() {super.onStopped();}@NonNull@Overridepublic Result doWork() {Data inputData = getInputData();String version = inputData.getString("version");//接口获取当前最新的信息//对比当前版本与服务器版本,是否需要更新//如果需要更新,返回更新下载的urlData outputData = new Data.Builder().putString("key_name", inputData.getString("download_url", "xxxxxx")).build();//设置输出数据setOutputData(outputData);return Result.success();}
}

这个时间太长了不好测试,不过是我之前自用的代码,没什么问题,哪天有时间做个Demo把日志文件导出来看看才能看出效果。

那除此之外我们一些Log的上传,图片的更新,资源或插件的下载等,我们都可以通过WorkManager来实现一些后台的操作,使用起来也是很简单。

总结

这里我直接给出了一些特定的场景应该使用哪一种定时任务,如果大家的应用场景适合App内部的定时任务,应该优先选择内部的定时任务。

App外的定时任务,都是系统服务的定时任务,不一定保险,毕竟是和厂商(特别是国内的厂商)作对,厂商会想方设法杀死我们的定时任务,毕竟有风险。

关于系统服务的定时任务我感觉自己讲的不是很好,好在给出了一些方案和一些文章,大家如果对一些基础的使用或者底层原理感兴趣,可以自行了解一下。

关于系统服务的周期任务的使用如果有错误,或者版本兼容的问题,又或者有更多或更好的方法,也可以在评论区交流讨论。

Android几种定时任务实现方式汇总相关推荐

  1. Android带三角形的弹窗,Android实现三角形气泡效果方式汇总

    在开发过程中,我们可能会经常遇到这样的需求样式: 这张图是截取京东消息通知的弹出框,我们可以看到右上方有个三角形的气泡效果,这只是其中一种,三角形的方向还可以是上.下.左.右. 通过截图可以发现,气泡 ...

  2. 【Python自动化任务】让运维更简单的7种定时任务实现方式,总有一种适合你的场景

    想要看更加舒服的排版.更加准时的推送 关注公众号"不太灵光的程序员" 每日八点有干货推送 有粉丝留言问什么时候可以写一个关于自动化任务的文章 准备上!~ 感觉有用关注公众号 &qu ...

  3. android 7种网络连接方式--IT蓝豹

    2019独角兽企业重金招聘Python工程师标准>>> 本项目由作者 王永飞 精心为初学者准备的学习项目. android 几种网络连接方式,本项目适合初学者学习网络知识. 项目中用 ...

  4. [ Android 五种数据存储方式之一 ] —— SharedPreferences存储数据

    SharedPreferences类,它是一个轻量级的存储类,特别适合用于保存软件配置参数. 主要是保存一些常用的配置比如窗口状态,一般在Activity中 重载窗口状态onSaveInstanceS ...

  5. HTML5 Web 客户端五种离线存储方式汇总

    为什么80%的码农都做不了架构师?>>>    最近折腾HTML5游戏需要离线存储功能,便把目前可用的几种HTML5存储方式研究了下,基于HT for Web写了个综合的实例,分别利 ...

  6. android坐标判断三角形,Android实现三角形气泡效果方式汇总

    在开发过程中,我们可能会经常遇到这样的需求样式: 这张图是截取京东消息通知的弹出框,我们可以看到右上方有个三角形的气泡效果,这只是其中一种,三角形的方向还可以是上.下.左.右. 通过截图可以发现,气泡 ...

  7. AE动画怎么导出?4种常见导出方式汇总

    今天要来跟大家分享的是AE动画常用的输出方式,这4种输出方式基本上能涵盖需求中的动画要求,废话不多说,咱们直接上干货! 01 Gifgun AEscripts GifGun是一款可以直接在AE里一键创 ...

  8. android 滑屏功能,Android 滑屏效果实现方式汇总

    Android的滑屏,最近在研究这个东西,发现新浪微博的客户端.腾讯微博的客户端都有这个功能.目前到今天为止,腾讯和微博的客户端都是使用viewPager控件写的,可能重写了一些地方.而腾讯老版本的q ...

  9. android气泡样式图片,Android实现三角形气泡效果方式汇总

    在开发过程中,我们可能会经常遇到这样的需求样式: 这张图是截取京东消息通知的弹出框,我们可以看到右上方有个三角形的气泡效果,这只是其中一种,三角形的方向还可以是上.下.左.右. 通过截图可以发现,气泡 ...

最新文章

  1. MySQL协议.NET Core实现(一)
  2. Understanding Unix/Linux Programming-终端控制和信号
  3. 【Android 逆向】Android 系统文件分析 ( /proc/ 目录文件分析 | 记录系统和进程信息 | version 内核版本信息文件 )
  4. 你觉得什么才是 Java 的基础知识?
  5. android学习笔记五。2、其他组件
  6. 12v小型电机型号大全_电动机型号参数大全,再也不怕看不懂电机型号了
  7. 真正的创业是什么感觉?
  8. 《C++游戏开发》笔记十二 战争迷雾:初步实现
  9. Visio画图如何保存高质量图片供论文使用
  10. Linux解压缩.tar.bz2
  11. 【机器学习基础】正规方程法(Normal equation)(正则化和非正则化)——吴恩达课程笔记
  12. Git Windows下配置Merge工具DiffMerge
  13. word文档被锁定,无法编辑怎么办?
  14. 不定长多项式展开后,展开式各阶系数的Java代码实现
  15. 玩游戏计算机缺失msvcp140,绝地求生计算机丢失MSVCP140.dll解决办法
  16. 计算机程序框图符号,数据流程图符号详解
  17. session 修改密码python_django修改密码强制退出机制
  18. P1413 坚果保龄球洛谷c++题解
  19. 个人网站申请域名怎么做?做网站申请域名多少钱?
  20. openssl心脏滴血漏洞

热门文章

  1. 谁买了方舟正版火影服务器,《方舟》火影服再次被玩家恶搞!漩涡鸣人:我不要面子的吗!...
  2. 腾创秒会达分布式无线全向麦克风MHD-G3B-13M分体式大功率扬声器
  3. 机器学习 AI 谷歌ML Kit 与苹果Core ML
  4. QT 单个窗口实现多页面切换
  5. Kali Linux渗透测试 073 扫描工具-Vega
  6. [UER#9]知识网络
  7. 关于企业上云,我们想跟你聊聊!|中机智库
  8. 路由器和交换机用什么线连接?
  9. python网易云爬虫_使用python进行爬虫下载网易云音乐
  10. iOS中的3种卡顿检测