Android前台服务讲解三关键类说明

1.如何创建通知?

/**
     * 创建服务通知
     */
    private fun createForegroundNotification(): Notification {
        val notificationBuidler = NotificationCompat.Builder(applicationContext, notificationChannelId)
        //通知小图标
        notificationBuidler.setSmallIcon(R.mipmap.ic_launcher_round)
        //通知标题
        notificationBuidler.setContentTitle("苏宁窖藏")
        //通知内容
        notificationBuidler.setContentText("苏宁是国内优秀的跨国企业?$count")
        //设置通知显示的时间
        notificationBuidler.setWhen(System.currentTimeMillis())
        //设定启动的内容
        val  activityIntent: Intent = Intent(this, MainActivity::class.java)
        activityIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        val pendingIntent: PendingIntent = PendingIntent.getActivity(this,
                1,activityIntent, PendingIntent.FLAG_UPDATE_CURRENT)
        notificationBuidler.setContentIntent(pendingIntent)
        //普通视图
        notificationBuidler.setCustomContentView(getContentView())
        //扩展视图
        notificationBuidler.setCustomBigContentView(getBigContentView())
        notificationBuidler.priority = NotificationCompat.PRIORITY_DEFAULT
        //设置为进行中的通知
        notificationBuidler.setOngoing(true)

        //创建通知并返回
        return notificationBuidler.build()
    }
//发送通知给状态栏显示
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.notify(NOTIFICATION_ID, notification)

1)借助Notification的帮助类NotificationCompat.Builder创建Notification;

2)设置Notification一些字段的值,例如:smallIcon,contentTitle,contentText,when等;

3)设置Notification点击以后的动作意图,由PendingIntent包裹,PendingIntent是主要包含Intent和一个要执行的目标动作,通过notificationBuidler.setContentIntent(pendingIntent)设置通知点击的目的Intent;

PendingInent对象获取方式不同,触发执行的意图也不同,触发后的意图总体来说有三种:进入某一Activity,发送广播,启动服务,分别调用PendingIntent.getActivity(...),PendingIntent.getBroadcast(...),PendingIntent.getService(...),PendingIntent.getActivities();在点击了通知栏的通知后,会触摸相应的意图,可以进入一个Activity,或发送一个广播,或启动一个服务。如果想实现一个自定义样式的通知,怎么做了?

4)创建RemoteViews,通过notificationBuidler.setCustomContentView(getContentView())设置要显示的自定义视图同时设置自定义视图点击意图;

RemoteViews构造方法中传入两个参数:包名,xml布局文件。这个xml布局文件对应如上通知中的界面,上面布局文件比较简单;

5)通过NotificationManager控制Notification显示notify()和移除canel();

 private fun getContentView(): RemoteViews{
        normalView = RemoteViews(this.packageName, R.layout.notify_content_view)
        //上一首图标添加点击监听
        val intent: Intent = Intent(PLAY_MUSIC)
        intent.component = ComponentName("com.yifan.service", "com.yifan.service.PlayBroadcastReceiver")
        val pendPreviousButtonIntent = PendingIntent.getBroadcast(this, count++, intent, PendingIntent.FLAG_UPDATE_CURRENT)
        normalView.setOnClickPendingIntent(R.id.play_last, pendPreviousButtonIntent);
        return normalView
    }

2.前台服务关键类有哪些?

2.1PendingIntent

PendingIntent是存放Intent和执行动作的意图,PendingIntent和Intent的区别就在于一个是立即执行的一个是在未来某个时候执行;

在Android开发中,PendingIntent主要用于Notification、AlarmManager以及Widget中,获取PendingIntent主要有三种方式:getActivity(),getService()以及getBroadcast(),这三种方式的参数都相同,但其中的第2个参数requestCode和第4个参数flag却不太好理解,第二个requestCode表示PendingIntent发送方的请求码,多数情况下设置为0即可,另外requestCode会影响到第四个参数flags的效果,这里结合Notification中的PendingIntent进行说明;

首先要明确一点,什么样的PendingIntent能够算是同一个PendingIntent。根据Google文档对PendingIntent的描述,当两个PendingIntent的类型为同一个(即两个同为getActivity()或getService()或getBroadcast()获取的)时,并且Intent的data、action、component、category、flag相同时(特别注意,Intent的extra不算),两个PendingIntent算是同一个。此外,第二个参数requestCode也可用来区分PendingIntent,因此即使两个PendingIntent的类型相同并且Intent相同,但如果requestCode不同的话,也算是两个不同的PendingIntent。明确上述后,还需对flag进行说明,这里主要讲解FLAG_CANCEL_CURRENT和FLAG_UPDATE_CURRENT

FLAG_CANCEL_CURRENT:若该PendingIntent已存在,则取消之前的PendingIntent,同时生成新的PendingIntent。

FLAG_UPDATE_CURRENT:若该PendingIntent已存在,则将该PendingIntent的Intent的extra数据更新为最新的。

因此,若一个Notification中指定了多个PendingIntent(deleteIntent、contentIntent、setOnClickPendingIntent),或多个Notification中都指定了PendingIntent,则如果存在相同的PendingIntent,则对于flag为FLAG_CANCEL_CURRENT时,之前的PendingIntent将被取消,Intent的内容无法传递,当前的PendingIntent不受影响;对于flag为FLAG_UPDATE_CURRENT时,该PendingIntent的Intent的extra数据将被更新为本次最新的,则之前的PendingIntent的extra数据被修改为本次最新的;

2.2RemoteViews

RemoteViews顾名思义就是远程View,它表示的是一个View结构,它可以在其他进程中显示,为了跨进程更新它的界面,RemoteViews提供了一组基础的操作来实现这个效果;RemoteViews在Android中的使用场景有两种:通知栏和桌面小部件;

创建RemoteViews对象我们只需要知道当前应用包名和布局文件的资源id,比较简单,但是要更新RemoteViews就不是那么容易了,因为我们无法直接访问布局文件中的View,而必须通过RemoteViews提供的特定的方法来更新View。比如设置TextView文本内容需要用setTextViewText方法,设置ImageView图片需要通过setImageViewResource方法。也可以给里面的View设置点击事件,需要使用PendingIntent并通过setOnClickPendingIntent方法来实现。之所以更新RemoteViews如此复杂,直接原因是因为RemoteViews没有提供跟View类似的findViewById这个方法,我们无法获取到RemoteViews中的子View;

2.3RemoteViews内部机制

RemoteViews的构造方法很多,我们最常见的一个是

 public RemoteViews(String packageName, int layoutId) {
        this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
    }

只需要包名和待加载的资源文件id,它并不能支持所有类型的View,也不支持自定义的View,它能支持的类型如下:

Layout:FrameLyout、LinearLayout、RelativeLayout、GridLayout

View:Button、ImageView、ImageButton、ProgressBar、TextView、ListView、GridView、StackView、ViewStub、AdapterViewFlipper、ViewFlipper、AnalogClock、Chronometer。

如果我们在RemoteViews中使用了它不支持的View不如EditText,那么就会发生异常。

我们看看RemoteViews的set方法

从这些方法中看出,原本可以直接调用的View的方法,现在要通过RemoteViews的一系列set方法来完成。

我们知道,通知栏和桌面小部件分别由NotificationManager和AppWidgetManager来管理的,而NotificationManager和AppWidgetManager是通过Binder分别和SystemServer进程中的NotificationManagerService以及AppWidgetService进行通信,因此,通知栏和桌面小部件中的布局文件实际上是在NotificationManagerService和AppWidgetService中被加载的,而他们运行在SystemServer中,这其实已经和我们自己的app进程构成了跨进程通信

 理论分析

首先RemoteViews会通过Binder传递到SystemServer进程,因为RemoteViews实现了Parcelable接口,可以跨进程传输,系统会根据RemoteViews中的包名等信息去获取到该app的资源,然后通过LayoutInflater去加载RemoteViews中的布局文件。在SystemServer进程中加载后的布局文件是一个普通的View,只不过对于我们的app进程来说,它是一个远程View也就是RemoteViews。接着系统会对View执行一系列界面更新任务,这些任务就是之前我们通过set方法提交的,set方法对View的更新操作并不是立刻执行的,在RemoteViews内部会记录所有的更新操作,具体的执行要等到RemoteViews被完全加载以后,这样RemoteViews就可以在SystemServer中进程中显示了,这就是我们所看到的通知栏消息和桌面小部件。当需要更新RemoteViews时,我们又需要调用一系列set方法通过NotificationManager和AppWidgetManager来提交更新任务,具体更新操作也是在SystemServer进程中完成的。

理论上讲系统完全可以通过Binder去支持所有的View和View操作,但是这样做代价太大,View的方法太多了,另外大量的IPC操作会影响效率。为了解决这个问题,系统并没有通过Binder去直接支持View的跨进程访问,而是提供了一个Action的概念,Action代表一个View操作,Action同样实现了Parcelable接口。系统首先将View操作封装到Action对象并将这些对象跨进程传输到远程进程,接着在远程进程中执行Action对象中的具体操作。在我们的app中每调用一次set方法,RemoteViews中就会添加一个对应的Action对象,当我们通过NotificationManager和AppWidgetManager来提交我们的更新时,这些Action对象就会传输到远程进程并在远程进程中依次执行。远程进程通过RemoteViews的apply方法来进行View的更新操作,apply方法内部是去遍历所有的Action对象并调用它们的apply方法,具体的View更新操作是由Action对象的apply方法来完成。

上述做法的好处,首先是不需要定义大量的Binder接口,其次通过在远程进程中批量执行RemoteViews的更新操作从而避免了大量的IPC操作,这就提高了程序的性能。

源码分析

首先我们从RemoteViews的set方法入手,比如设置图片的方法setImageViewResource它内部实现是这样的:

    /**
     * Equivalent to calling {@link ImageView#setImageResource(int)}
     *
     * @param viewId The id of the view whose drawable should change
     * @param srcId The new resource id for the drawable
     */
    public void setImageViewResource(int viewId, int srcId) {
        setInt(viewId, "setImageResource", srcId);
    }

上面的代码中viewId是被操作的View的id,setImageResource是方法名,srcId是要给这个ImageView设置的图片资源id。这里的方法名和ImageView的setImageResource是一致的。我们再看看setInt方法的具体实现:

    /**
     * Call a method taking one int on a view in the layout for this RemoteViews.
     *
     * @param viewId The id of the view on which to call the method.
     * @param methodName The name of the method to call.
     * @param value The value to pass to the method.
     */
    public void setInt(int viewId, String methodName, int value) {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.INT, value));
    }

可以看到它内部并没有对View进行直接操作,而是添加了一个ReflectionAction对象,字面上理解应该是一个反射类型的动作,再看addAction的实现:

/**
     * Add an action to be executed on the remote side when apply is called.
     *
     * @param a The action to add
     */
    private void addAction(Action a) {
        if (hasLandscapeAndPortraitLayouts()) {
            throw new RuntimeException("RemoteViews specifying separate landscape and portrait" +
                    " layouts cannot be modified. Instead, fully configure the landscape and" +
                    " portrait layouts individually before constructing the combined layout.");
        }
        if (mActions == null) {
            mActions = new ArrayList<>();
        }
        mActions.add(a);
    }

上述代码可以看到,在RemoteViews内部维护了一个名为mActions的ArrrayList,所有的对View更新的操作动作都被添加到这个集合中,注意,仅仅是添加进来保存,并没有去执行这些Action。到这里setImageViewResource方法的源码已经结束了,下面我们要弄清楚这些Action的执行。我们再看看RemoteViews的apply方法:

    /** @hide */
    public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
        RemoteViews rvToApply = getRemoteViewsToApply(context);

        View result = inflateView(context, rvToApply, parent);
        rvToApply.performApply(result, parent, handler);
        return result;
    }

首先RemoteViews会通过LayoutInflater去加载它的布局文件,加载完之后通过performApply方法去执行一些更新操作。

   private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
        if (mActions != null) {
            handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler;
            final int count = mActions.size();
            for (int i = 0; i < count; i++) {
                Action a = mActions.get(i);
                a.apply(v, parent, handler);
            }
        }
    }

这里遍历了mActions集合且执行每个Action的apply方法,应该可以看出,Action的apply方法才是真正操作View更新的地方。

当我们调用RemoteViews的set方法时,并不会立刻更新它们的界面,而必须要通过NotificationManager的notify方法以及AppWidgetManager的updateAppWidget方法才能更新它们的界面。实际上在AppWidgetManager的updateAppWidget内部实现中,的确是通过RemoteViews的apply方法和reapply方法来加载或更新界面的,apply和reapply的区别在于:apply会加载布局并更新界面,而reapply则只会更新界面,初始化时调用apply方法,后面的更新则调用reapply方法。

ReflectionAction是Action的子类,我们看下它的源码:

/**
 * Base class for the reflection actions.
 */
private final class ReflectionAction extends Action {

    String methodName;
    int type;
    Object value;

    ReflectionAction(int viewId, String methodName, int type, Object value) {
        this.viewId = viewId;
        this.methodName = methodName;
        this.type = type;
        this.value = value;
    }

    @Override
    public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
        final View view = root.findViewById(viewId);
        if (view == null) return;

        Class<?> param = getParameterType();
        if (param == null) {
            throw new ActionException("bad type: " + this.type);
        }

        try {
            getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));
        } catch (ActionException e) {
            throw e;
        } catch (Exception ex) {
            throw new ActionException(ex);
        }
    }
}

它的内部实现有点长,我们主要看它的apply方法。

getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));

这句代码就是它以反射的方式来对View进行操作,getMethod根据方法名得到反射所需的Method对象,然后执行该方法。

RemoteViews中的单击事件,只支持发起PendingIntent,不支持onClickListener这种方法。我们需要注意setOnClickPendingIntent、setPendingIntentTemplate和setOnClickFillInIntent这几个方法之间的区别和联系。setOnClickPendingIntent是用于给普通的View设置点击事件,但是它不能给ListView或者GridView、StackView中的item设置点击事件,因为开销比较大,系统禁止了这种方式。而setPendingIntentTemplate方法就能给item设置单击事件,具体使用请参照这篇文章Android 之窗口小部件高级篇--App Widget 之 RemoteViews

2.4RemoteViews的优缺点

实际开发中,跨进程通信我们可以选择AIDL去实现,但是如果对界面的更新比较频繁,这时会有效率问题,而且AIDL接口可能会变得很复杂,但如果采用RemoteViews来实现就没有这个问题了,RemoteViews的缺点就是它仅支持一些常见的View,而对于自定义View是不支持的。

参考:

Android中的RemoteViews - 简书

基于android的网络音乐播放器-通知栏控制(RemoteViews)(十)_xgq330409675的博客-CSDN博客

Android中的RemoteViews - 简书

猜你喜欢

转载自blog.csdn.net/ahou2468/article/details/122527344