使用IntentService解决点我达骑手APP消息提醒机制中的优先级排序问题

王春蕾

最新版的点我达骑手APP对新订单消息提醒、订单信息被修改、新的可抢订单等声音及制动提醒做出了规范和优化,加入了消息提醒(包括声音及振动提醒)优先级机制,即在优先级较高的声音在播放过程中有优先级较低的声音插入播放队列,则忽略优先级较低的声音不进行播放;优先级较低的声音在播放过程中有优先级较高的声音插入播放队列,则在优先级较低的声音播放结束后再对插入的优先级较高的声音进行播放。本文从如何更安全高效的执行耗时任务(音频播放等),如何有序执行任务队列中的任务等方面进行介绍。

1.怎么播放音频文件?

这里说的"怎么播放音频文件"并不是指用什么API区播放,而是在Android客户端APP在应用内存被限制的情况下(具体APP被分配的内存与Android系统版本、手机厂商对Android系统的个性化定制、手机硬件配置等有关)如何保证在播放音频文件的过程中尽可能不损耗应用的性能从而产生ANR、Crash等重大性能问题。

首先贴一下后文中介绍的三种播放音频文件方式公用的工具类代码,不喜勿喷。

package audio.rider.dwd.com.audioplayer.util;

import android.content.Context;  
import android.media.MediaPlayer;  
import android.util.Log;

import audio.rider.dwd.com.audioplayer.R;

/**
 * Created by WangChunelei on 16/10/28.
 * 音频播放工具类
 */
public class AudioManager {  
    private static final String TAG = "AudioManager";
    private static AudioManager singleton;
    /*正在播放的音频优先级*/
    private int audioPriority;
    /*回调对象*/
    private OnAudioChangeListener onAudioChangeListener;
    /*当前音频播放的次数*/
    private int currentRepeatIndex = 0;
    /*声音播放器对象*/
    private MediaPlayer mediaPlayer = null;
    /*声音的播放次数*/
    int audioRepeatCount = 0;

    private AudioManager() {
    }

    public static AudioManager getInstance() {
        if (singleton == null) {
            synchronized (AudioManager.class) {
                if (singleton == null) {
                    singleton = new AudioManager();
                }
            }
        }

        return singleton;
    }

    /**
     * 播放音频文件
     *
     * @param context
     * @param audioPriority 提醒优先级
     */
    public void playAudio(Context context, int audioPriority) {
        this.audioPriority = audioPriority;
        this.currentRepeatIndex = 0;
        this.audioRepeatCount = 0;

        android.media.AudioManager audioManager = (android.media.AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        int current = audioManager.getStreamVolume(android.media.AudioManager.STREAM_MUSIC);
        int maxcurrent = audioManager.getStreamMaxVolume(android.media.AudioManager.STREAM_MUSIC);
        while (current < maxcurrent) {
            audioManager.adjustStreamVolume(android.media.AudioManager.STREAM_MUSIC, android.media.AudioManager.ADJUST_RAISE, 0);
            current = audioManager.getStreamVolume(android.media.AudioManager.STREAM_MUSIC);
        }

        switch (audioPriority) {
            case 1:
                mediaPlayer = MediaPlayer.create(context, R.raw.voice_1);
                audioRepeatCount = 2;
                break;
            case 2:
                mediaPlayer = MediaPlayer.create(context, R.raw.voice_2);
                audioRepeatCount = 3;
                break;
            case 3:
                mediaPlayer = MediaPlayer.create(context, R.raw.voice_3);
                audioRepeatCount = 3;
                break;
            default:
                break;
        }


        if (onAudioChangeListener != null) {
            onAudioChangeListener.onAudioStartPlaying();
        }
        //音频文件播放结束后的回调

        mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mp) {
                Log.e(TAG, "currentThread:" + Thread.currentThread().getName());
                try {
                    //播放指定次数的音频文件
                    if (currentRepeatIndex < audioRepeatCount - 1) {
                        mediaPlayer.start();
                        currentRepeatIndex++;
                    } else {
                        //播放完成
                        if (onAudioChangeListener != null) {
                            onAudioChangeListener.onAudioFinished();
                        }
                    }
                } catch (Exception e) {
                }
            }
        });

        mediaPlayer.start();
    }

    /**
     * 获取正在播放的音频文件类型
     *
     * @return
     */
    private int getPlayingAudioType() {
        return audioPriority;
    }

    /**
     * 设置状态切换监听器
     *
     * @param onAudioChangeListener
     */
    public void setOnAudioChangeListener(OnAudioChangeListener onAudioChangeListener) {
        this.onAudioChangeListener = onAudioChangeListener;
    }

    /**
     * 音频文件播放过程中状态切换的回调
     */
    public interface OnAudioChangeListener {
        //开始播放音频文件
        void onAudioStartPlaying();

        //播放完成
        void onAudioFinished();
    }

}
1.主线程执行消息提醒任务

Android系统的主线程又称为UI线程,一般情况下,在UI线程多以处理UI变更的工作居多,耗时任务建议不要放在UI线程处理,否则很容易产生UI卡顿、ANR等性能问题,对应用的流畅性有严重影响。

那么问题来了,在主线程执行循环播放声音的任务,是否也会造成应用卡顿,产生性能和流畅性的损耗呢?

在Activity添加一个点击事件并直接调用一下方法,就可以循环播放类型为1的音频文件了。

事实上直接这么使用并未对性能和流畅性产生明显影响,是不是意味着播放音频文件的工作在底层实现上并未放在主线程呢?我们查看MediaPlayer的start方法,可以很容易翻到下面的几行代码:

从代码中可以清晰地看到,其实底层在播放音频文件的时候是在native层进行的,也就是C/C++层,因为在播放音频文件的过程中需要调用相关硬件设备并执行加载设备驱动等工作。

通过这种方式实现的效果是:在声音播放结束之后会立即执行进行下一次播放。如果现在需求修改成:在每次播放结束间隔2s后再进行下一次的播放,那么这种方式是否仍然没有问题呢?

最容易想到的实现方式就是让线程休眠2s。

既然是让线程休眠,那么这个工作就一定不能放在主线程了,因为在线程休眠的过程中应用会直接卡顿做不了任何操作。解决问题的思路就自然是自定义子线程去处理播放音频及休眠间隔了。

2.新建Thread执行消息提醒任务

将上面代码中的声音结束监听并播放下一次声音的代码中增加线程休眠时间。代码截图如下:

然后新建一个Thread类,在run()中调用播放声音的方法,Thread中代码如下:

在Activity中新建该线程的实例并调用start方法开启子线程。运行之后很快就会发现,在声音播放结束的2秒休眠期间,UI也会直接卡顿。事实上,通过查看日志可以看到,MediaPlayer的onCompletion回调其实是直接在主线程进行的。

那我们就需要改变一下声音循环播放的实现方式了,通过for循环+Thread休眠的方式同样也可以很容易的实现这一效果,该部分代码逻辑演化成下图所示:

只不过这个休眠的时间需要做一下处理,也就是在执行完mediaPlayer.start()后立即进入线程休眠阶段,休眠的时间是声音的播放时长+2s,也就是声音在播放过程中线程也同步休眠,声音播放完成后线程仍有2s的休眠剩余时间,等休眠时间结束后,进入下一次的循环。

虽然通过Thread的方式解决了目前的产品需求且对应用的性能和流畅性没有产生影响,但是也会有一些令人头疼的后续问题,比如在任务执行结束后怎样安全退出线程以减少内存消耗、这种实现方式怎么解决因优先级排序带来的任务插入问题等问题。问题肯定是可以解决的,但却是如履薄冰,线程使用过程中稍有不当就会出现各种直接导致应用Crash的问题。

那么有没有一种任务结束自动关闭、临时插入任务自动排队且使用过程中没有过多“雷区”的解决方案呢?IntentService就派上用场了。

3.使用IntentService执行消息提醒任务

做过Android开发的对Service一定不陌生,Service作为Android四大组件之一,充当着计算型组件的角色,主要用于在后台处理一些任务,比如点我达骑手Android客户端的用户经纬度上传就是Service后台进行处理的。

Service在后台处理任务,但"后台"并不等于"异步",Service默认也是在UI线程进行的,所以对于一些耗时、耗性能的工作如果需要在Service处理的话,也是需要在Service中单独开线程进行处理的。

IntentService是Service的子类,但却与Service有着较大的区别:

1.IntentService默认创建一个独立的工作线程来处理任务,对应用性能几乎没影响

2.IntentService具有任务依次处理的"排队"功能(其实就是Looper轮询和线程的等待-唤醒机制实现的),可以依次执行任务队列中的任务。

3.任务完成后Service自动销毁,无法担心空任务Service对应用性能的影响  

IntentService的使用方式与普通Service比较像,比如都需要在AndroidManifest.xml中进行声明,都可以用startService的方式启动等。不过IntentService需要一个默认的构造函数,任务的执行是在onHandleIntent内部进行的,这也是与普通Service使用过程中的一些不同点。

新建的IntentService代码如下:

package audio.rider.dwd.com.audioplayer;

import android.app.IntentService;  
import android.content.Intent;  
import android.util.Log;

import audio.rider.dwd.com.audioplayer.util.AudioManager;

/**
 * Created by WangChunLei on 16/11/1.
 */
public class AudioPlayerIntentService extends IntentService {  
    private static final String TAG = "AudioIntentService";

    public AudioPlayerIntentService() {
        super("AudioPlayerIntentService");
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.e(TAG, "onCreate");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        //声音类型
        int audioType = intent.getIntExtra("AUDIO_TYPE", 0);
        Log.e(TAG, "currentThread:" + Thread.currentThread().getName());
        Log.e(TAG, "onHandleIntent");
        AudioManager.getInstance().playAudio(getApplicationContext(), audioType);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.e(TAG, "onDestroy");
    }
}

然后在Activity通过startService的方式即可启动该IntentService,即可实现在子线程中进行的、且在任务结束后自动关闭的、有休眠间隔的声音循环播放逻辑,代码的执行顺序如下:

2.怎么解决消息提醒机制的优先级排序问题?

上文介绍了IntentService的特性,这也是消息提醒机制优先级排序问题的解决方案。

模拟一个在声音播放过程中有优先级较高的声音进入播放队列的过程,代码如下:

代码的执行顺序如下:

至此,任务的排序问题就解决了。