Android定时任务的应用及实现

定时任务在Android中这算是一个常用的功能了。比如注册获取验证码时的倒计时,或者支付时候也会有倒计时。正计时大多也都用在定时唤醒或者延时做一些操作的情况。本文我会先整理一下定时任务的几种方法以及CountDownTimer这个专门用来倒计时的类,后面我们以最常用应用场景来演示一下验证码倒计时原理的实现,包装效果后如下图:

实现正定时的五种方法

方法1:通过Handler + Thread 的方式。代码如下。

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private static final int TIMER = 999;
    private static boolean flag = true;

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

        setTimer();
    }

    private void setTimer(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (flag){
                    try {
                        Thread.sleep(1000); //休眠一秒
                        mHanler.sendEmptyMessage(TIMER);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    private Handler mHanler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case TIMER:
                    //去执行定时操作逻辑
                    break;
                default:
                    break;
            }
        }
    };

     private void stopTimer(){
         flag = false;
     }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        stopTimer();
    } 
}

这是比较容易想到的方法,值得注意的是:如果在此Activity关闭之前,必须要终止线程内的循环,否则就会造成内存泄露

方法2:通过Handler + Message的方式。代码如下。

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private static final int TIMER = 999;
    private static boolean flag = true;

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

        setTimer();
    }

    private void setTimer(){
        Message message = mHandler.obtainMessage(TIMER);     // Message
        mHandler.sendMessageDelayed(message, 1000);
    }

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case TIMER:
                    //在这里去执行定时操作逻辑
                    
                    if (flag) {
                        Message message = mHandler.obtainMessage(TIMER);
                        mHandler.sendMessageDelayed(message, 1000);
                    }
                    break;
                default:
                    break;
            }
        }
    };

    private void stopTimer(){
         flag = false;
    }
}

这个方法来实现定时其实还是有些缺陷的,主要的问题不是看起来像是死循环,而是在执行定时操作之后才可以进行下一次定时启动,如果此操作是耗时操作了,必定会延后下一秒的启动。所以这个方法定时严格来说不精确,不推荐。

方法3:通过Handler + Runnable的方式。代码如下。

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private static boolean flag = true;

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

        setTimer();
    }

    private void setTimer(){
        mHandler.postDelayed(runnable, 1000);
    }

    private Handler mHandler = new Handler();

    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            //在这里执行定时需要的操作
            
            if (flag) {
                mHandler.postDelayed(this, 1000);
            }
        }
    };

    private void stopTimer(){
        flag = false;
    }

}

此方法和方法2很相似,缺点也相似,优点嘛~~简单!

方法4:通过Handler + TimerTask的方式。代码如下。

MainActivity.java

private static final int TIMER = 999;

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

        setTimer();
    }

    private void setTimer(){
        timer.schedule(task, 1000, 1000);       // timeTask
    }

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case TIMER:
                //在此执行定时操作
                    
                    break;
                default:
                    break;
            }
        }
    };
    
    Timer timer = new Timer();

    TimerTask task = new TimerTask() {
        @Override
        public void run() {
            mHandler.sendEmptyMessage(TIMER);
        }
    };

    private void stopTimer(){
        timer.cancel();
        task.cancel();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        stopTimer();
    }
}

这里引用了timer和task两个新的类,配合handler使用是目前比较多的方式,but我也不推荐,因为下面这个是我推荐的封装方法。
MyTimeTask.java

public class MyTimeTask {
    private Timer timer;
    private TimerTask task;
    private long time;

    public MyTimeTask(long time, TimerTask task) {
        this.task = task;
        this.time = time;
        if (timer == null){
            timer=new Timer();
        }
    }
    
    public void start(){
        timer.schedule(task, 0, time);//每隔time时间段就执行一次
    }

    public void stop(){
        if (timer != null) {
            timer.cancel();
            if (task != null) {
                task.cancel();  //将原任务从队列中移除
            }
        }
    }
}

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private static final int TIMER = 999;
    private MyTimeTask task;

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

        setTimer();
    }

    private void setTimer(){
        task =new MyTimeTask(1000, new TimerTask() {
            @Override
            public void run() {
                mHandler.sendEmptyMessage(TIMER);
                //或者发广播,启动服务都是可以的
            }
        });
        task.start();
    }

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case TIMER:
                //在此执行定时操作

                    break;
                default:
                    break;
            }
        }
    };

    private void stopTimer(){
        task.stop();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        stopTimer();
    }
}

单独封装出来的定时任务工具,引入方式简单,方便取消停止定时任务。简单来说优点就是:把工具类直接复制粘贴,然后简单调用就能用!

方法5:采用AlarmManger实现长期精确的定时任务。

此方法设计需要细说的点有很多,不再列代码,最好是各位亲自去写一写,对此方法印象会深一些,可以给出一个非常常见的应用场景:每隔6个小时,检查一遍数据库内容,查看数据是否有变化,如果有则弹出显示框显示,若没有则无变化。

AlarmManger常用方法有三个:

set(int type,long startTime,PendingIntent pi)//一次性
setExact(int type, long triggerAtMillis, PendingIntent operation)//一次性的精确版
setRepeating(int type,long startTime,long intervalTime,PendingIntentpi)//精确重复
setInexactRepeating(int type,long startTime,longintervalTime,PendingIntent pi);//非精确,降低功耗

各个参数含义:type表示闹钟类型,startTime表示闹钟第一次执行时间,long intervalTime表示间隔时间,PendingIntent表示闹钟响应动作。

type闹钟类型分为:

    AlarmManager.ELAPSED_REALTIME:休眠后停止,相对开机时间
    AlarmManager.ELAPSED_REALTIME_WAKEUP:休眠状态仍可唤醒cpu继续工作,相对开机时间
    AlarmManager.RTC:同1,但时间相对于绝对时间
    AlarmManager.RTC_WAKEUP:同2,但时间相对于绝对时间
    AlarmManager.POWER_OFF_WAKEUP:关机后依旧可用,相对于绝对时间

startTime:闹钟的第一次执行时间,以毫秒为单位,一般使用当前时间。
intervalTime:执行时间间隔。
PendingIntent :PendingIntent用于描述Intent及其最终的行为.,这里用于获取定时任务的执行动作。

常见使用方式:利用AlarmManger+Service+BarocastReceiver来实现可唤醒cpu,甚至实现精确定时,适用于配合service在后台执行一些长期的定时行为

实现倒计时的方法

倒计时几种方法呢,这个不像正计时那样简单实现容易想到的那5种,甚至正计时通过总量减的方式全可以转化成倒计时。所以在此我想介绍另一个专门倒计时的类CountDownTimer来讲解。

构造方法:

         CountDownTimer (long millisInFuture, long countDownInterval)
         //millisInFuture:设置倒计时的总时间(毫秒)
         //countDownInterval:设置每次减去多少毫秒

验证码获取的示例代码如下:
MainActivity.java

public class MainActivity extends AppCompatActivity {
    private static final int LEFT_TIME = 999;
    private static final int TIME_OVER = 998;
    private Button bt_verify;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bt_verify = (Button) findViewById(R.id.bt_verify);
        bt_verify.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                setTimer();
            }
        });
    }

    private void setTimer(){
       timer.start();
    }

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case LEFT_TIME:
                    long left_time = (long) msg.obj;
                    bt_verify.setEnabled(false);//禁止button点击
                    bt_verify.setText((left_time / 1000) + " S");
                    break;
                case TIME_OVER:
                    bt_verify.setEnabled(true);
                    bt_verify.setText("点击重发");
                    break;
                default:
                    break;
            }
        }
    };

    private void stopTimer(){
        timer.cancel();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        stopTimer();
    }

    long millisInFuture = 60*1000;
    long countDownInterval =1000;
    CountDownTimer timer = new CountDownTimer(millisInFuture,countDownInterval) {

        @Override
        public void onTick(long millisUntilFinished) {
            //millisUntilFinished  剩余时间回调,这个是实时的(以countDownInterval为单位)
            Message msg = Message.obtain();
            msg.what = LEFT_TIME;
            msg.obj = millisUntilFinished;
            mHandler.sendMessage(msg);
        }

        @Override
        public void onFinish() {
            //结束时的回调
            mHandler.sendEmptyMessage(TIME_OVER);
        }
    };
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="5dp"
        android:layout_marginRight="5dp">

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_toLeftOf="@+id/bt_verify"
            android:layout_marginRight="5dp"/>

        <Button
            android:id="@+id/bt_verify"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="获取验证码"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"/>
    </RelativeLayout>
</RelativeLayout>

需要注意的是:CountDownTimer如果使用不当,常常会报空指针异常,甚至造成严重的内存泄漏,不过解决办法也是有的:

一是在CountDownTimer的onTick方法中记得判空:
activity中:

    if(!activity.isFinishing()){
        //doing something...
    }

fragment中:

    if(getActivity()!=null){
       //doing something...
    }

二还需要在宿主Activity或fragment生命周期结束的时候,记得调用timer.cancle()方法:

    @Override
    public void onDestroy() {
        if(timer!=null){
            timer.cancel();
            timer = null;
        }
        super.onDestroy();
    }

总结:

短期的定时任务推荐用最后两种方式方式实现,长期或者有精确要求的定时任务则选择AlarmManger + Service在后台执行。最后把开始的展示图去壳之后内部实现了一遍,内容比较简单,整篇文中最值得注意的OOM问题是开发中常常忽视的,减少OOM和ANR的情况,才可以将代码的细节把控好!

来源:  https://www.jianshu.com/p/9304c8faf79f

 

另外, alarmManger相关情况如下:

系统服务之定时服务(AlarmManager)

AlarmManager提供了对系统定时服务的访问接口,使得开发者可以安排在未来的某个时间运行应用。当到达闹铃设定时间,系统就会广播闹铃之前注册的Intent。如果此时目标应用没有被启动,系统还会帮你自动启动目标应用。即使设备已经进入睡眠已注册的闹铃也会被保持,只有当设备关闭或是重启的时候会被清除。下面基于Android 8.0源码来一起学习一下。

闹铃类型

AlarmManager中一共提供了四种闹钟类型,前两种对应的System.currentTimeMillis()(系统当前时间)时间,后两种对应SystemClock.elapsedRealtime()(系统运行时间)时间,以WAKEUP结尾的类型能够唤醒设备,其他的类型不能唤醒设备,直到设备被唤醒才能出发警报提醒。

    public static final int RTC_WAKEUP = 0;
    public static final int RTC = 1;
    public static final int ELAPSED_REALTIME_WAKEUP = 2;
    public static final int ELAPSED_REALTIME = 3;

设置时间

在AlarmMananger中提供了setTime和setTimeZone方法分别用来设置系统时间和系统默认时区。其中,设置系统时间需要"android.permission.SET_TIME"权限。

返回值公开方法
voidsetTime(long millis)
voidsetTimeZone(String timeZone)

设置闹铃

返回值公开方法
voidset(int type, long triggerAtMillis, PendingIntent operation)
voidset(int type, long triggerAtMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)
voidsetAlarmClock(AlarmManager.AlarmClockInfo info, PendingIntent operation)
voidsetAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)
voidsetExact(int type, long triggerAtMillis, PendingIntent operation)
voidsetExact(int type, long triggerAtMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)
voidsetExactAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)
voidsetInexactRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)
voidsetRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)
voidsetWindow(int type, long windowStartMillis, long windowLengthMillis, PendingIntent operation)
voidsetWindow(int type, long windowStartMillis, long windowLengthMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)

以上是AlarmManager中提供的所有设置闹铃的方法,下面来详细介绍一下。

  • set
返回值公开方法
voidset(int type, long triggerAtMillis, PendingIntent operation)
voidset(int type, long triggerAtMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)

用于设置一次性闹铃,执行时间在设置时间附近,为非精确闹铃。方法一和方法二的区别:到达设定时间时方法一会广播PendingIntent中设定的Intent,而方法二会直接回调OnAlarmListener 中的onAlarm()方法。

  • setExact
返回值公开方法
voidsetExact(int type, long triggerAtMillis, PendingIntent operation)
voidsetExact(int type, long triggerAtMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)
voidsetAlarmClock(AlarmManager.AlarmClockInfo info, PendingIntent operation)

用于设置一次性闹铃,执行时间更为精准,为精确闹铃。方法一和二的区别参见上面set的区别。setAlarmClock方法等同于通过setExact方法设置的RTC_WAKEUP类型的闹铃,所以把他归在setExact中介绍。其中AlarmClockInfo实现了Android序列化接口Parcelable,里面包含了mTriggerTime(执行时间)和mShowIntent(执行动作)两个成员变量,可以看做是对闹铃事件的一个封装类。

  • setInexactRepeating和setRepeating
返回值公开方法
voidsetInexactRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)
voidsetRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)

setInexactRepeating和setRepeating两种方法都是用来设置重复闹铃的,setRepeating执行时间更为精准。在Android 4.4之后,Android系统为了省电把时间相近的闹铃打包到一起进行批量处理,这就使得setRepeating方法设置的闹铃不能被精确的执行,必须要使用setExact来代替。

  • setAndAllowWhileIdle和setExactAndAllowWhileIdle
返回值公开方法
voidsetAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)
voidsetExactAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)

使用setAndAllowWhileIdle和setExactAndAllowWhileIdle方法设置一次闹铃,可以在低功耗模式下被执行,setExactAndAllowWhileIdle执行时间更为精准。手机灭屏以后会进入低功耗模式(low-power idle modes),这个时候你会发现通过setExact设置的闹铃也不是100%准确了,需要用setExactAndAllowWhileIdle方法来设置,闹铃才能在低功耗模式下被执行。

  • setWindow
返回值公开方法
voidsetWindow(int type, long windowStartMillis, long windowLengthMillis, PendingIntent operation)
voidsetWindow(int type, long windowStartMillis, long windowLengthMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)

用于设置某个时间段内的一次闹铃。比如,我想在下午的2点到4点之间设置一次提醒。两个方法的区别同set。

取消闹铃

返回值公开方法
voidcancel(PendingIntent operation)
voidcancel(AlarmManager.OnAlarmListener listener)

用于取消设置过的闹铃,分别对应于PendingIntent和AlarmManager.OnAlarmListener方式注册的闹铃。

获得下一次闹铃事件

返回值公开方法
AlarmManager.AlarmClockInfogetNextAlarmClock()

用于获得下一次闹铃事件。

常用时间定义

AlarmManager类已经帮我们定义好了常用的时间常量。

    public static final long INTERVAL_FIFTEEN_MINUTES = 15 * 60 * 1000;
    public static final long INTERVAL_HALF_HOUR = 2*INTERVAL_FIFTEEN_MINUTES;
    public static final long INTERVAL_HOUR = 2*INTERVAL_HALF_HOUR;
    public static final long INTERVAL_HALF_DAY = 12*INTERVAL_HOUR;
    public static final long INTERVAL_DAY = 2*INTERVAL_HALF_DAY;

源码分析

设置闹铃

通过深入分析AlarmManager的源码,发现上面提到的所有与闹铃设置有关的方法(setXXX)最终都会调用setImpl方法,区别在于不同的应用场景设置的参数不同。setImpl方法的源码如下:

    private void setImpl(@AlarmType int type, long triggerAtMillis, long windowMillis,
            long intervalMillis, int flags, PendingIntent operation, final OnAlarmListener listener,
            String listenerTag, Handler targetHandler, WorkSource workSource,
            AlarmClockInfo alarmClock) {
        // 处理非法时间的设置
        if (triggerAtMillis < 0) {
            triggerAtMillis = 0;
        }

        // 把 OnAlarmListener 封装起来
        ListenerWrapper recipientWrapper = null;
        if (listener != null) {
            synchronized (AlarmManager.class) {
                if (sWrappers == null) {
                    sWrappers = new ArrayMap<OnAlarmListener, ListenerWrapper>();
                }

                recipientWrapper = sWrappers.get(listener);
                // no existing wrapper => build a new one
                if (recipientWrapper == null) {
                    recipientWrapper = new ListenerWrapper(listener);
                    sWrappers.put(listener, recipientWrapper);
                }
            }

            final Handler handler = (targetHandler != null) ? targetHandler : mMainThreadHandler;
            recipientWrapper.setHandler(handler);
        }
        // 调用Service的set方法
        try {
            mService.set(mPackageName, type, triggerAtMillis, windowMillis, intervalMillis, flags,
                    operation, recipientWrapper, listenerTag, workSource, alarmClock);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

那么这个mService是哪来的呢?通过搜索在SystemServiceRegistry类中找到了一段静态方法,SystemServiceRegistry是用来管理所有能由Context.getSystemService方法获得系统服务的类,通过ServiceManager.getServiceOrThrow根据服务名字来查找到对应的IBinder,进而生成IAlarmManager实例并作为参数传递给AlarmManager。mService就是这个IAlarmManager实例。

    android-8.1.0_r2/frameworks/base/core/java/android/app/SystemServiceRegistry.java
    static {  
           ...
       registerService(Context.ALARM_SERVICE, AlarmManager.class,
                new CachedServiceFetcher<AlarmManager>() {
            @Override
            public AlarmManager createService(ContextImpl ctx) throws ServiceNotFoundException {
                IBinder b = ServiceManager.getServiceOrThrow(Context.ALARM_SERVICE);
                IAlarmManager service = IAlarmManager.Stub.asInterface(b);
                return new AlarmManager(service, ctx);
            }});
          ...
    }

接下来看看 IAlarmManager 是由谁实现的呢?熟悉Android源码的同学自然而然就会想到有可能有一个AlarmManagerService类来提供具体的实现机制。搜一搜还真有这个类,进一步搜索 IAlarmManager 查看哪里实现了这个接口类(当然,熟悉AIDL机制的同学也可以直接搜索 IAlarmManager ,也能找到)。

    private final IBinder mService = new IAlarmManager.Stub() {
        // 设置闹铃的方法
        @Override
        public void set(String callingPackage,
                int type, long triggerAtTime, long windowLength, long interval, int flags,
                PendingIntent operation, IAlarmListener directReceiver, String listenerTag,
                WorkSource workSource, AlarmManager.AlarmClockInfo alarmClock) {
            final int callingUid = Binder.getCallingUid();

            // make sure the caller is not lying about which package should be blamed for
            // wakelock time spent in alarm delivery
            mAppOps.checkPackage(callingUid, callingPackage);

            // Repeating alarms must use PendingIntent, not direct listener
            if (interval != 0) {
                if (directReceiver != null) {
                    throw new IllegalArgumentException("Repeating alarms cannot use AlarmReceivers");
                }
            }

            if (workSource != null) {
                getContext().enforcePermission(
                        android.Manifest.permission.UPDATE_DEVICE_STATS,
                        Binder.getCallingPid(), callingUid, "AlarmManager.set");
            }

            // No incoming callers can request either WAKE_FROM_IDLE or
            // ALLOW_WHILE_IDLE_UNRESTRICTED -- we will apply those later as appropriate.
            flags &= ~(AlarmManager.FLAG_WAKE_FROM_IDLE
                    | AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED);

            // Only the system can use FLAG_IDLE_UNTIL -- this is used to tell the alarm
            // manager when to come out of idle mode, which is only for DeviceIdleController.
            if (callingUid != Process.SYSTEM_UID) {
                flags &= ~AlarmManager.FLAG_IDLE_UNTIL;
            }

            // If this is an exact time alarm, then it can't be batched with other alarms.
            if (windowLength == AlarmManager.WINDOW_EXACT) {
                flags |= AlarmManager.FLAG_STANDALONE;
            }

            // If this alarm is for an alarm clock, then it must be standalone and we will
            // use it to wake early from idle if needed.
            if (alarmClock != null) {
                flags |= AlarmManager.FLAG_WAKE_FROM_IDLE | AlarmManager.FLAG_STANDALONE;

            // If the caller is a core system component or on the user's whitelist, and not calling
            // to do work on behalf of someone else, then always set ALLOW_WHILE_IDLE_UNRESTRICTED.
            // This means we will allow these alarms to go off as normal even while idle, with no
            // timing restrictions.
            } else if (workSource == null && (callingUid < Process.FIRST_APPLICATION_UID
                    || callingUid == mSystemUiUid
                    || Arrays.binarySearch(mDeviceIdleUserWhitelist,
                            UserHandle.getAppId(callingUid)) >= 0)) {
                flags |= AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED;
                flags &= ~AlarmManager.FLAG_ALLOW_WHILE_IDLE;
            }
            // 最终会调用AlarmManagerService的setImpl方法
            setImpl(type, triggerAtTime, windowLength, interval, operation, directReceiver,
                    listenerTag, flags, workSource, alarmClock, callingUid, callingPackage);
        }
        ...
    };

从上面这段源码可以看出,set方法最终会调用AlarmManagerService的setImpl方法。在setImpl中,根据传递的参数经过一系列的计算,传递给setImplLocked方法进行下一步处理。

    void setImpl(int type, long triggerAtTime, long windowLength, long interval,
            PendingIntent operation, IAlarmListener directReceiver, String listenerTag,
            int flags, WorkSource workSource, AlarmManager.AlarmClockInfo alarmClock,
            int callingUid, String callingPackage) {
        ...
        synchronized (mLock) {
            ...
            setImplLocked(type, triggerAtTime, triggerElapsed, windowLength, maxElapsed,
                    interval, operation, directReceiver, listenerTag, flags, true, workSource,
                    alarmClock, callingUid, callingPackage);
        }
    }

setImplLocked方法会把传递过来的参数封装成一个Alarm对象,调用setImplLocked的另一个重载方法。在setImplLocked中,会去计算Alarm所属的批次(Batch),然后根据批次进行重新打包,打包后对内核Alarm进行重新规划,更新下一个Alarm时间。

    private void setImplLocked(int type, long when, long whenElapsed, long windowLength,
            long maxWhen, long interval, PendingIntent operation, IAlarmListener directReceiver,
            String listenerTag, int flags, boolean doValidate, WorkSource workSource,
            AlarmManager.AlarmClockInfo alarmClock, int callingUid, String callingPackage) {
        // 参数封装成一个Alarm对象
        Alarm a = new Alarm(type, when, whenElapsed, windowLength, maxWhen, interval,
                operation, directReceiver, listenerTag, workSource, flags, alarmClock,
                callingUid, callingPackage);
        try {
            if (ActivityManager.getService().isAppStartModeDisabled(callingUid, callingPackage)) {
                Slog.w(TAG, "Not setting alarm from " + callingUid + ":" + a
                        + " -- package not allowed to start");
                return;
            }
        } catch (RemoteException e) {
        }
        removeLocked(operation, directReceiver);
        setImplLocked(a, false, doValidate);
    }

    private void setImplLocked(Alarm a, boolean rebatching, boolean doValidate) {
        // 计算alarm所属的批次
        int whichBatch = ((a.flags&AlarmManager.FLAG_STANDALONE) != 0)
                ? -1 : attemptCoalesceLocked(a.whenElapsed, a.maxWhenElapsed);
        if (whichBatch < 0) {
            Batch batch = new Batch(a);
            addBatchLocked(mAlarmBatches, batch);
        } else {
            Batch batch = mAlarmBatches.get(whichBatch);
            if (batch.add(a)) {
                // The start time of this batch advanced, so batch ordering may
                // have just been broken.  Move it to where it now belongs.
                mAlarmBatches.remove(whichBatch);
                addBatchLocked(mAlarmBatches, batch);
            }
        }
       ...
        if (!rebatching) {
            ...
            if (needRebatch) {
                // 重新打包所有的Alarm
                rebatchAllAlarmsLocked(false);
            }
            // 重新规划内核的Alarm
            rescheduleKernelAlarmsLocked();
            // 更新下一个Alarm的时间
            updateNextAlarmClockLocked();
        }
    }

在rescheduleKernelAlarmsLocked方法中会调用setLocked方法,setLocked方法内部会去调用native方法set,最终把Alarm设置到内核中去。

    private void setLocked(int type, long when) {
        if (mNativeData != 0) {
            // The kernel never triggers alarms with negative wakeup times
            // so we ensure they are positive.
            long alarmSeconds, alarmNanoseconds;
            if (when < 0) {
                alarmSeconds = 0;
                alarmNanoseconds = 0;
            } else {
                alarmSeconds = when / 1000;
                alarmNanoseconds = (when % 1000) * 1000 * 1000;
            }
            // native方法
            set(mNativeData, type, alarmSeconds, alarmNanoseconds);
        } else {
            Message msg = Message.obtain();
            msg.what = ALARM_EVENT;

            mHandler.removeMessages(ALARM_EVENT);
            mHandler.sendMessageAtTime(msg, when);
        }
    }

取消闹铃

在AlarmManager提供了两个cancel方法来取消闹铃,调用时候需要传递一个PendingIntent或是OnAlarmListener实例作为参数,从此也可以看出闹铃服务内部是以PendingIntent或是OnAlarmListener作为区分不同闹铃的唯一标识的。cancel(PendingIntent operation) 和 cancel(OnAlarmListener listener) 的实现原理是差不多的,最终都会调用mService.remove方法来移除闹铃,这里以 cancel(PendingIntent operation) 方法为例进行详细分析。

    public void cancel(PendingIntent operation) {
        // 如果 PendingIntent 为空,在N和之后的版本会抛出空指针异常
        if (operation == null) {
            final String msg = "cancel() called with a null PendingIntent";
            if (mTargetSdkVersion >= Build.VERSION_CODES.N) {
                throw new NullPointerException(msg);
            } else {
                Log.e(TAG, msg);
                return;
            }
        }

        try {
            // mService是一个IBinder,由来及对应方法的实现同上面设置闹铃中的解析
            mService.remove(operation, null);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

mService.remove方法中首先会去判断PendingIntent 和 IAlarmListener 是否都为空,有一个不为空则调用removeLocked继续进行处理。

    private final IBinder mService = new IAlarmManager.Stub() {
        ...
         // 取消闹铃的方法
        @Override
        public void remove(PendingIntent operation, IAlarmListener listener) {
            // PendingIntent 和 IAlarmListener 必须有一个不为空
            if (operation == null && listener == null) {
                Slog.w(TAG, "remove() with no intent or listener");
                return;
            }
            synchronized (mLock) {
                // 调用AlarmManagerService中的removeLocked方法
                removeLocked(operation, listener);
            }
        }
        ...
    };

在removeLocked方法中,会根据传递过来的参数在mAlarmBatches和mPendingWhileIdleAlarms两个列表中查询当前要删除的Alarm,如果匹配到则删除。删除后会对所有闹铃重新打包,如果删除的是非低功耗模式下启动的闹铃则需要刷新非低功耗下启动的闹铃设置,最后更新下一次闹铃时间。

   private void removeLocked(PendingIntent operation, IAlarmListener directReceiver) {
        // 遍历查询并删除匹配的Alarms
        boolean didRemove = false;
        for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
            Batch b = mAlarmBatches.get(i);
            didRemove |= b.remove(operation, directReceiver);
            if (b.size() == 0) {
                mAlarmBatches.remove(i);
            }
        }
        for (int i = mPendingWhileIdleAlarms.size() - 1; i >= 0; i--) {
            if (mPendingWhileIdleAlarms.get(i).matches(operation, directReceiver)) {
                // Don't set didRemove, since this doesn't impact the scheduled alarms.
                mPendingWhileIdleAlarms.remove(i);
            }
        }

        if (didRemove) {
            if (DEBUG_BATCH) {
                Slog.v(TAG, "remove(operation) changed bounds; rebatching");
            }
            boolean restorePending = false;
            if (mPendingIdleUntil != null && mPendingIdleUntil.matches(operation, directReceiver)) {
                mPendingIdleUntil = null;
                restorePending = true;
            }
            if (mNextWakeFromIdle != null && mNextWakeFromIdle.matches(operation, directReceiver)) {
                mNextWakeFromIdle = null;
            }
            // 重新打包所有的闹铃
            rebatchAllAlarmsLocked(true);
            if (restorePending) {
                // 重新存储非低功耗下启动的闹铃
                restorePendingWhileIdleAlarmsLocked();
            }
            // 更新下一次闹铃时间
            updateNextAlarmClockLocked();
        }
    }

最终在restorePendingWhileIdleAlarmsLocked方法中会调用rescheduleKernelAlarmsLocked和updateNextAlarmClockLocked 重新规划内核的Alarm并更新下一个Alarm的时间。

    void restorePendingWhileIdleAlarmsLocked() {
        ...
        // 重新规划内核的Alarm
        rescheduleKernelAlarmsLocked();
        // 更新下一个Alarm的时间
        updateNextAlarmClockLocked();
        ...
    }

获得下一次闹铃事件

AlarmManager提供了getNextAlarmClock方法来获得下一次闹铃事件,该方法中会把当前的UserId作为查询依据传递到AlarmManagerService中的getNextAlarmClockImpl方法,从而查询出当前用户所对应的下一次闹铃事件。

    frameworks/base/core/java/android/app/AlarmManager.java
    public AlarmClockInfo getNextAlarmClock() {
        return getNextAlarmClock(UserHandle.myUserId());
    }

    public AlarmClockInfo getNextAlarmClock(int userId) {
        try {
            return mService.getNextAlarmClock(userId);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    frameworks/base/services/core/java/com/android/server/AlarmManagerService.java
    private final IBinder mService = new IAlarmManager.Stub() {
        ...
         // 获得下次闹铃事件
        @Override
        public AlarmManager.AlarmClockInfo getNextAlarmClock(int userId) {
            userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
                    Binder.getCallingUid(), userId, false /* allowAll */, false /* requireFull */,
                    "getNextAlarmClock", null);

            return getNextAlarmClockImpl(userId);
        }
        ...
    };

    AlarmManager.AlarmClockInfo getNextAlarmClockImpl(int userId) {
        synchronized (mLock) {
            return mNextAlarmClockForUser.get(userId);
        }
    }

设置系统时间

设置系统时间的功能实现流程比较简单,在AlarmManager提供的setTime方法中直接调用mService.setTime方法,进而通过AlarmManagerService中声明的native方法setKernelTime把时间设置到底层内核中去。

    frameworks/base/core/java/android/app/AlarmManager.java
    public void setTime(long millis) {
        try {
            mService.setTime(millis);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    frameworks/base/services/core/java/com/android/server/AlarmManagerService.java
    private final IBinder mService = new IAlarmManager.Stub() {
        ...
         // 设置系统时钟的方法
        @Override
        public boolean setTime(long millis) {
            // 注意,设置系统时间需要"android.permission.SET_TIME"权限
            getContext().enforceCallingOrSelfPermission(
                    "android.permission.SET_TIME",
                    "setTime");

            if (mNativeData == 0) {
                Slog.w(TAG, "Not setting time since no alarm driver is available.");
                return false;
            }

            synchronized (mLock) {
                // native 方法,直接设置到底层kernel中
                return setKernelTime(mNativeData, millis) == 0;
            }
        }
        ...
    };

设置系统时区

在AlarmManager提供的setTimeZone方法中直接调用mService的setTimeZone方法,进而调用AlarmManagerService的setTimeZoneImpl方法,并由此方法完成整个系统时区设置的相关逻辑(包括系统属性值修改、设置内核时区和广播系统时区变化)。

    frameworks/base/core/java/android/app/AlarmManager.java
    public void setTimeZone(String timeZone) {
        ...
        try {
            mService.setTimeZone(timeZone);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    frameworks/base/services/core/java/com/android/server/AlarmManagerService.java
    private final IBinder mService = new IAlarmManager.Stub() {
        ...
         // 设置系统默认时区的方法
        @Override
        public void setTimeZone(String tz) {
            getContext().enforceCallingOrSelfPermission(
                    "android.permission.SET_TIME_ZONE",
                    "setTimeZone");

            final long oldId = Binder.clearCallingIdentity();
            try {
                setTimeZoneImpl(tz);
            } finally {
                Binder.restoreCallingIdentity(oldId);
            }
        }
        ...
    };

    void setTimeZoneImpl(String tz) {
        ...
        boolean timeZoneWasChanged = false;
        synchronized (this) {
            String current = SystemProperties.get(TIMEZONE_PROPERTY);
            if (current == null || !current.equals(zone.getID())) {
                timeZoneWasChanged = true;
                // 设置SystemProperties中时区对应的字段值
                SystemProperties.set(TIMEZONE_PROPERTY, zone.getID());
            }

            int gmtOffset = zone.getOffset(System.currentTimeMillis());
            // native 方法,直接设置到底层kernel中
            setKernelTimezone(mNativeData, -(gmtOffset / 60000));
        }

        TimeZone.setDefault(null);
        // 广播系统时区变化
        if (timeZoneWasChanged) {
            Intent intent = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
            intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
                    | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
                    | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
            intent.putExtra("time-zone", zone.getID());
            getContext().sendBroadcastAsUser(intent, UserHandle.ALL);
        }
    }

总结

本文基于Android 8.0源码,首先列举了闹铃种类和设置闹铃、取消闹铃、获得下一次闹铃事件、设置系统时间、设置系统时区等相关方法,然后结合系统源码详细分析了各种方法的实现机制。使用过程中有以下几点需要注意:
1、设置系统时间需要"android.permission.SET_TIME"权限。
2、每当有新的Alarm设置或删除定时服务都会重新计算所属批次,把时间相近的Alarm打包到一个批次里(Batch)一起执行,起到优化电池节省耗电的目的。这就是导致非精确Alarm执行时间存在不确定误差的根本原因。
3、如果想要在低耗电模式下触发闹铃需要通过setAndAllowWhileIdle和setExactAndAllowWhileIdle方法来设置闹铃。
4、如果设置的闹铃时间已经过了,闹铃会被立即触发。这个问题可以通过比较闹铃设置时间和当前时间来解决。
5、根据实际需求选择是否设置精确闹铃以达到优化电池节省耗电的目的。
6、通过设置时区的源码可知,如果想要获取系统时区的相关信息可以通过监听Intent.ACTION_TIMEZONE_CHANGED广播或是直接读取系统属性TIMEZONE_PROPERTY。

具体的使用方法可以参考下面两个链接:
使用示例:Android定时器AlarmManager
不同版本的差异:关于使用AlarmManager的注意事项

 -----------

关于android的alarmmanager使用过程中的坑(包括魅族手机休眠后无法启动闹钟的问题)

  在最近的开发过程中,需要完成一个本地闹钟的功能,需求是固定的3个闹钟,每日重复,可以手动开关,需要时间精准,闹钟响起时需弹出dialog和播放音乐。
在alarmmanager的使用过程中主要参考了github上的一个demo , Android-AlarmManagerClock ,遇到了一些比较难解决的问题,现在整理一下以防止以后再采坑。

1.关于不同手机版本精确闹钟的兼容

SDK API < 19

一般情况下,使用 AlarmManager 来执行重复定时任务的代码如下所示:

alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), TIME_INTERVAL, pendingIntent);

或者

alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), TIME_INTERVAL, pendingIntent);

setRepeating 该方法用于设置重复定时任务。

  • 第一个参数表示闹钟类型:一般为 AlarmManager.ELAPSED_REALTIME_WAKEUP 或者 AlarmManager.RTC_WAKEUP 。它们之间的区别就是前者是从手机开机后的时间,包含了手机睡眠时间;而后者使用的就是手机系统设置中的时间。所以如果设置为 AlarmManager.RTC_WAKEUP ,那么可以通过修改手机系统的时间来提前触发定时事件。另外,对于相似的 AlarmManager.ELAPSED_REALTIME 和 AlarmManager.RTC 来说,它们不会唤醒 CPU 。所以使用的频率较少;
  • 第二个参数表示任务首次执行时间:与第一个参数密切相关。第一个参数若为 AlarmManager.ELAPSED_REALTIME_WAKEUP ,那么当前时间就为 SystemClock.elapsedRealtime() ;若为 AlarmManager.RTC_WAKEUP ,那么当前时间就为 System.currentTimeMillis() ;
  • 第三个参数表示两次执行的间隔时间:这个参数没什么好讲的,一般为常量;
  • 第四个参数表示对应的响应动作:一般都是去发送广播,然后在广播接收 onReceive(Context context, Intent intent) 中做相关操作。

SDK API >= 19 && SDK API < 23

当你写好代码、满心欢喜地将程序跑在手机上的时候,傻眼了!你会发现在 Android 4.4 及以上版本的定时任务不是按照规定时间间隔来执行的。比如你设置了每隔 3 分钟发出一个 HTTP 请求,结果你一看莫名其妙地变成了隔 5 分钟发一次。

然后你查阅 Android 官网中关于 Android 4.4 API 会看到如下几句话:

1017209-9aeac689a801b0a7.png

恍然大悟!原来是 Google 为了追求系统省电,所以“偷偷加工”了一下唤醒的时间间隔。但也正如上面官网中所说的那样,如果在 Android 4.4 及以上的设备还要追求精准的闹钟定时任务,要使用 setExact() 方法。

所以,相应的代码就变成了这样:

// pendingIntent 为发送广播
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), pendingIntent);
} else {
    alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), TIME_INTERVAL, pendingIntent);
}

private BroadcastReceiver alarmReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 重复定时任务
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TIME_INTERVAL, pendingIntent);
        }
        // to do something
        doSomething();
    }
};

当你写好了“加强版”的 AlarmManager 之后,内心肯定无比小激动。这下总应该行了吧?运行一下,果然没错!在 Android 4.4 上的确按照规定的时间间隔在执行任务。哈哈,这下大功告成了!!!

SDK API >= 23

在 Android 4.4 上品尝到胜利的甜头后,你顺便在 Android 6.0 的设备上测试了一下。结果。。。。。。你又 TMD 傻眼了!

发现在设备关屏静止一段时间后, AlarmManager 又又又不能正常工作了。相必此时你连日狗的心都有了吧!强忍着泪水,再次打开 Android 官网中关于 Android 6.0 变更 ,发现在 Android 6.0 中引入了低电耗模式和应用待机模式。然后接着往下看 对低电耗模式和应用待机模式进行针对性优化 ,发现会有下面一段话:

1017209-958b1bca16fe2651.png

啊啊啊啊啊啊!之前在 Android 4.4 上能用的 setExact() 方法在 Android 6.0 上因为低电耗模式又不能正常使用了。但是,Google 又又又提供了新的方法 setExactAndAllowWhileIdle() 来解决在低电耗模式下的闹钟触发。

所以,Attention!相关的代码又被改写为这样:

// pendingIntent 为发送广播
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), pendingIntent);
} else {
    alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), TIME_INTERVAL, pendingIntent);
}

private BroadcastReceiver alarmReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 重复定时任务
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TIME_INTERVAL, pendingIntent);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TIME_INTERVAL, pendingIntent);
        }
        // to do something
        doSomething();
    }
};

2.Android 7.0 pendingIntent用intent传递的bug

问题比较少见,只有你在跨进程传递数据的时候会碰到,如pendingIntent中

在7.0中通过pendingIntent的bundle传递的数据时,你会发现serializable和parcelable的数据拿不到

如果你只传了string,那是没问题的,但是如果你传了string和一个serializable你会发现,不光serializable拿不到,连string也拿不到了,黑人问好脸吧

原因参考:

https://commonsware.com/blog/2016/07/22/be-careful-where-you-use-custom-parcelables.html

解决方案1:

https://stackoverflow.com/questions/18000093/how-to-marshall-and-unmarshall-a-parcelable-to-a-byte-array-with-help-of-parcel/18000094#18000094

他的解决方法很独特,将所有数据转为基本类型bytes再去传递,当然,这可以解决我们的问题

然后我就想既然byte能解决,只传string也没问题,那为什么不干脆直接传string嗯,所以另一个简单的修改方法就是把所有的对象全部转成jsonstring去传递,也可以解决问题

解决方案2:

继续寻找有没有更加合适的方案时发现google的issuetracker中有个大神发现了一个更简单的方法,直接把所有数据放到bundle里,然后将bundler作为参数传递即intent.putExtra("data",bundle)也可以解决问题,

详情参考:

https://issuetracker.google.com/issues/37097877

参考代码:

        Intent intent = new Intent(ALARM_ACTION);
        Bundle bundle = new Bundle();
        bundle.putInt(AlarmConfig.DATA_BUNDLE_ALARM_ID,id);
        bundle.putLong(AlarmConfig.DATA_BUNDLE_INTERVALMILLIS, intervalMillis);
        intent.putExtra(AlarmConfig.DATA_BUNDLE, bundle);
        // 这个pendingIntent是Intent的包装类, 在闹钟到点的时候能够发送广播给AlarmOnTimeReceiver
        PendingIntent sender = PendingIntent.getBroadcast(context, id, intent, PendingIntent
                .FLAG_CANCEL_CURRENT);

另外两个问题是关于魅族手机适配的问题:
发现在魅族手机上出现2个问题,魅族M6版本 Flyme 6.1.4.6A ,Android 版本 7.1.2 :

  1. 实现一个通知栏通知的功能,闹钟响起来就出现通知通知栏,setContentText()中的内容要是有 感叹号(!) 就通知就出现不了,代码如下 :
    //发送通知
    private void showNotification(AlarmClock clock) {
    int NOTIFICATION_ID = clock.getId();
    NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
    builder.setContentTitle
    ("闹钟响起了").setContentText("时间到了哦!").setAutoCancel(true).setColor(Color
    .rgb(134, 177, 194)).setSmallIcon(R.mipmap.ic_launcher).setLargeIcon
    (BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)).setDefaults
    (NotificationCompat.DEFAULT_ALL);
    //要跳转到主页面
    Intent intent = new Intent(this, HomeActivity.class);
    PendingIntent operation = PendingIntent.getActivity(this, NOTIFICATION_ID, intent, 0);
    builder.setFullScreenIntent(operation, false); // 悬挂式Notification(5.0 新增)
    builder.setContentIntent(operation);
    Notification notification = builder.build();
    mNotificationManager.notify(NOTIFICATION_ID, notification);
    }

2.使用AlarmManager ,在App中按Home键锁屏后闹钟失效(只有按Home键锁屏这一种情况,按返回键没有这个问题) , AlarmManager接收不到广播 ,导致闹钟失效

解决问题1最终还是没有解决
解决问题2:通过魅族手机-设置-关闭智能休眠模式

魅族手机的问题说明 手机厂商改动系统太多了, 给开发者带来了无数适配的坑.... = =

参考文章:
关于使用AlarmManager的注意事项
Android 7.0 pendingIntent bug(AlarmManager通过PendingIntent传递数据(跨进程数据传递)

来源: https://www.jianshu.com/p/6be84993d2f7