一、前言


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

 

二、RemoteViews 的使用


2.1 通知栏

使用系统默认的样式弹出一个通知的方式,代码如下:(android3.0之后)

private void showDefaultNotification() {
    NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
    // 设置通知的基本信息:icon、标题、内容
    builder.setSmallIcon(R.mipmap.ic_launcher);
    builder.setContentTitle("My notification");
    builder.setContentText("Hello World!");
    builder.setAutoCancel(true);
    // 设置通知的点击行为:这里启动一个 Activity
    Intent intent = new Intent(this, SecondActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    builder.setContentIntent(pendingIntent);
    // 发送通知 id 需要在应用内唯一
    NotificationManager manager= (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    manager.notify(id, builder.build());
}

 上述代码会弹出一个系统默认样式的通知,单击通知后会打开 SecondActivity 同时会清除本身。通知栏除了默认的效果外还支持自定义布局。实现自定义通知我们首先需要提供一个布局文件,然后通过 RemoteViews 来加载这个布局文件即可改变通知的样式。样例代码如下:

private void showCustomNotification() {
    RemoteViews remoteView;
    // 构建 remoteView
    remoteView = new RemoteViews(getPackageName(), R.layout.layout_notification);
    remoteView.setTextViewText(R.id.tvMsg, "哈shenhuniurou");
    remoteView.setImageViewResource(R.id.ivIcon, R.mipmap.ic_launcher_round);
    NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
    // 设置自定义 RemoteViews
    builder.setContent(remoteView).setSmallIcon(R.mipmap.ic_launcher);
    // 设置通知的优先级(悬浮通知)
    builder.setPriority(NotificationCompat.PRIORITY_MAX);
    Uri alarmSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
    // 设置通知的提示音
    builder.setSound(alarmSound);
    // 设置通知的点击行为:这里启动一个 Activity
    Intent intent = new Intent(this, SecondActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    builder.setContentIntent(pendingIntent);
    builder.setAutoCancel(true);
    Notification notification = builder.build();
    NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    manager.notify(1001, notification);
}

效果如下图所示:

android refreshLayout最新版本 android remoteviews_布局文件

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

 

2.2 桌面小部件

 

 

 

 

 

三、PendingIntent


在 Android 中,我们常常使用 PendingIntent 来表达一种“留待日后处理”的意思。从这个角度来说,PendingIntent 可以被理解为一种特殊的异步处理机制。不过,单就命名而言,PendingIntent 其实具有一定误导性,因为它既不继承于 Intent,也不包含 Intent,它的核心可以粗略地汇总成四个字——“异步激发”。很明显,这种异步激发常常是要跨进程执行的。比如说 A 进程作为发起端,它可以从系统“获取”一个 PendingIntent,然后 A 进程可以将 PendingIntent 对象通过 binder 机制传递给 B 进程,再由 B 进程在未来某个合适时机,“回调” PendingIntent 对象的 send() 动作,完成激发。

PendingIntent 是 Android 提供的一种用于外部程序调起自身程序的能力,生命周期不与主程序相关。外部程序通过 PendingIntent 只能调用起三种组件:Activity、Service、Broadcast。使用场景有三个:使用 AlarmManager 设定闹钟、在系统状态栏显示 Notification、在桌面显示 Widget。PendingIntent 也只能通过下列的静态方法获取:

// 获取 Broadcast 关联的 PendingIntent
PendingIntent.getBroadcast(Context context, int requestCode, Intent intent, int flags)
// 获取 Activity 关联的 PendingIntent
PendingIntent.getActivity(Context context, int requestCode, Intent intent, int flags)
PendingIntent.getActivity(Context context, int requestCode, Intent intent, int flags, Bundle options)
// 获取 Service 关联的 PendingIntent
PendingIntent.getService(Context context, int requestCode, Intent intent, int flags)

上面的 getActivity() 的意思其实是,获取一个 PendingIntent 对象,而且该对象日后激发时所做的事情是启动一个新 activity。也就是说,当它异步激发时,会执行类似 Context.startActivity() 那样的动作。相应地,getBroadcast() 和 getService() 所获取的 PendingIntent 对象在激发时,会分别执行类似 Context.sendBroadcast() 和 Context.startService() 这样的动作。

PendingIntent 是系统对于待处理数据的一个引用,称之为:token。当主程序被 Killed 时,token 还是会继续存在的,可以继续供其他进程使用。如果要取消 PendingIntent,需要调用 PendingIntent 的 cancel 方法。对于 PendingIntent 容易误解的一点是:如果创建了很多 PendingIntent,只要 extra 中的数据不同的话,以为就是两个不同的 PendingIntent 这种理解是错误的。Extras不参与 Intent 的匹配过程。正确区分不同 PendingIntent 有两种方法:

  • PendingIntent.getXXX() 方法中的 requestCode 不同
  • 通过 Intent.filterEquals 测试时不相等

关于 PendingIntent.getXXX() 方法中第四个参数 flags,在 PendingIntent 定义了四个比较常用的 FLAG:

FLAG_CANCEL_CURRENT

如果新请求的 PendingIntent 发现已经存在时,取消已存在的,用新的 PendingIntent 替换。

FLAG_NO_CREATE

如果新请求的 PendingIntent 发现已经存在时,忽略新请求的,继续使用已存在的。日常开发中很少使用。

FLAG_ONE_SHOT

表示 PendingIntent 只能使用一次,如果已使用过,那么 getXXX(...) 将会返回 NULL ,也就是说同类的通知只能使用一次,后续的通知单击后将无法打开。

FLAG_UPDATE_CURRENT

如果新请求的 PendingIntent 发现已经存在时, 如果 Intent 有字段改变了,就更新已存在的 PendingIntent。

 

四、RemoteViews 解析


RemoteViews 的作用是在其他进程中显示并更新 View 界面,为了更好地理解它的内部机制,我们先来看一下它的主要功能。首先看一下它的构造方法,这里只介绍一个最常用的构造方法:

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

它接收两个参数,第一个表示当前应用的包名,第二个参数表示待加载的布局文件 id。RemoteViews 所支持 View 所有类型如下:

Layout

FrameLyout、LinearLayout、RelativeLayout、GridLayout

View

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

RemoteViews 目前并不能支持所有的 View 类型,RemoteViews 不支持它们的子类以及其他 View 类型,也无法使用自定义 View。比如我们在 RemoteViews 中使用系统的 EditText,那么将会抛出异常。RemoteViews 没有提供 findViewById 方法,因此无法直接访问里面的 View 元素,而必须通过 RemoteViews 所提供的一系列 set 方法来完成,当然这是因为 RemoteViews 是在远程进程中显示,所以没办法直接 findViewById。下表列举了部分常用的 set 方法:

android refreshLayout最新版本 android remoteviews_加载_02

从这些方法中看出,原本可以直接调用的 View 的方法,现在要通过 RemoteViews 的一系列 set 方法来完成。而且从方法的声明上来看,很像是通过反射来完成的,事实上大部分的 set 方法的确是通过反射来完成的。

下面描述一下 RemoteViews 的内部机制。我们知道,通知栏和桌面小部件分别由 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 对象就会传输到远程进程并在远程进程中依次执行。这个过程可以参看下图:

android refreshLayout最新版本 android remoteviews_加载_03

远程进程通过 RemoteViews 的 apply 方法来进行 View 的更新操作,apply 方法内部是去遍历所有的 Action 对象并调用它们的 apply 方法,具体的 View 更新操作是由 Action 对象的 apply 方法来完成。上述做法的好处,首先是不需要定义大量的 Binder 接口,其次通过在远程进程中批量执行 RemoteViews 的更新操作从而避免了大量的 IPC 操作,这就提高了程序的性能。

上面从理论上分析了 RemoteViews 的内部机制,接下来我们从源码的角度再来分析 RemoteViews 的工作流程。首先我们从 RemoteViews 的 set 方法入手,比如 setTextViewText,它内部实现是这样的:

public void setTextViewText(int viewId, CharSequence text) {
    setCharSequence(viewId, "setText", text);
}

在上面的代码中,viewId 是被操作的 View 的 id,"setText" 是方法名,text 是要给 TextView 设置的文本,这里联想一下 TextView 的 setText 方法,是不是很一致呢?接着再看 setCharSequence 的实现,代码如下:

public void setCharSequence(int viewId, String methodName, CharSequence value) {
    addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}

从 setCharSequence 的实现可以看出,它的内部并没有对 View 进程直接的操作,而是添加了一个 ReflectionAction 对象,从名字来看,这应该是一个反射类型的动作。再看 addAction 的实现,代码如下:

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<Action>();
    }
    mActions.add(a);
    // update the memory usage stats
    a.updateMemoryUsageEstimate(mMemoryUsageCounter);
}

上述代码可以看到,在 RemoteViews 内部维护了一个名为 mActions 的 ArrrayList,外界每调用一次 set 方法,RemoteViews 就会为其创建一个 Action 对象并加入到这个 ArrayList 中。注意,仅仅是将 Action 对象添加进来保存,并没有去执行这些对 View 实际操作的 Action。到这里 setTextViewText 方法的源码已经结束了,下面我们要弄清楚这些 Action 的执行。首先看一下 RemoteViews 的 apply 方法:

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

    View result = inflateView(context, rvToApply, parent);
    loadTransitionOverride(context, handler);

    rvToApply.performApply(result, parent, handler);

    return result;
}

private View inflateView(Context context, RemoteViews rv, ViewGroup parent) {
    final Context contextForResources = getContextForResources(context);
    Context inflationContext = new RemoteViewsContextWrapper(context, contextForResources);

    LayoutInflater inflater = (LayoutInflater)
            context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

    inflater = inflater.cloneInContext(inflationContext);
    inflater.setFilter(this);
    View v = inflater.inflate(rv.getLayoutId(), parent, false);
    v.setTagInternal(R.id.widget_frame, rv.getLayoutId());
    return v;
}

从上面代码可以看出,首先会通过 LayoutInflater 去加载 RemoteViews 中的布局文件,RemoteViews 中的布局文件可以通过 getLayoutId 这个方法获得,加载完布局文件后会通过 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 方法。每一次的 set 操作都会对应它里面的一个 Action 对象,Action 对象的 apply 方法就是真正操作 View 的地方。

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

我们再继续看一些 Action 的子类的具体实现,首先看一下 RelectionAction 的具体实现,代码如下:

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);
        }
    }
}

通过上述代码可以发现,ReflectionAction 表示的是一个反射动作,通过它对 View 的操作会以反射的方式来调用,其中 getMethod 就是根据方法名来得到反射所需的 Method 对象。使用 ReflectionAction 的 set 方法有:setTextViewText、seetBoolean、setLong、setDouble 等。除了 ReflectionAction,还有其他 Action,比如 TextViewSizeAction、ViewPaddingAction、SetOnClickPendingIntent 等。这里再分析一下 TextViewSizeAction,它的代码如下:

private class TextViewSizeAction extends Action {
        public TextViewSizeAction(int viewId, int units, float size) {
            this.viewId = viewId;
            this.units = units;
            this.size = size;
        }

        ...
       
        @Override
        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
            final TextView target = root.findViewById(viewId);
            if (target == null) return;
            target.setTextSize(units, size);
        }

        public String getActionName() {
            return "TextViewSizeAction";
        }

        int units;
        float size;

        public final static int TAG = 13;
}

TextViewSizeAction 的实现比较简单,它之所以不用反射来实现,是因为 setTextSize 这个方法有 2 个参数,因此无法复用 ReflectionAction,因为 ReflectionAction 的反射调用只有一个参数。

RemoteViews 中的单击事件,只支持发起 PendingIntent,不支持 onClickListener 这种方法。我们需要注意 setOnClickPendingIntent、setPendingIntentTemplate 和 setOnClickFillInIntent 这几个方法之间的区别和联系。setOnClickPendingIntent 是用于给普通的 View 设置点击事件,但是它不能给 ListView 或者 GridView、StackView 中的 item 设置点击事件,因为开销比较大,系统禁止了这种方式。而如果要给 ListView 和 StackView 中的 item 添加单击事件,则必须将 setPendingIntentTemplate 和 setOnClickFillInIntent 组合使用才可以。

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