RemoteViews的应用和原理

RemoveViews的应用和原理

《Android开发艺术探索读书笔记》

概念

RemoteView是一个可以在其他进程中显示的View结构。

RemoteViews提供了一组基础的操作用于跨进程更新它的界面。

应用场景

  1. 通知栏

  2. 桌面小部件

 

RemoteViews的应用

通知栏——NotificationManager使用RemoteViews实现自定义视图的效果

桌面小部件——AppWidgetProvider使用RemoteViews实现桌面小部件的视图

由于二者的View(通知栏的自定义视图和桌面小部件的视图)都运行在SystemServer进程中,因此无法直接在Activity(主进程)中更新View

RemoteViews提供了一系列set方法来控制跨进程更新界面,解决了二者更新View的困难。

 

RemoteViews在通知栏上的应用

创建RemoteViews来实现自定义布局,然后设置到notification中,随后就能通过RemoteViewsset方法来更新视图。

RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);
remoteViews.setTextViewText(R.id.msg, "custom_notification");// 设置自定义视图的TextView的值
remoteViews.setImageViewResource(R.id.icon, R.drawable.icon);// 设置自定义视图的ImageView的值
PendingIntent j2Main = PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnclickPendingIntent(R.id.button, j2Main);
​
Notification notification = new NotificationCompat.Builder(this, "custom_view")
    .setCustomContentView(remoteViews)//设置自定义视图
    ...
    .build();

 

RemoteViews在桌面小部件上的应用

AppWidgetProviderAndroid提供的用于实现桌面小部件的类,本质是一个广播,即BroadcastReceiver

桌面小部件的开发步骤

  1. 定义小部件界面R.layout.widget

  2. 定义小部件配置信息res/xml/appwidget_provider_info.xml,包括指定小部件使用的初始化布局,设定小部件的最小尺寸 和 小部件的自动更新周期(单位毫秒)。

  3. 定义小部件的实现类AppWidgetProvider

  4. AndroidManifest.xml中声明小部件(作为receiver)

小部件实现类常用的方法:

  1. onEnabled:当小部件第一次添加到桌面时被调用,可以添加多次,但只在第一次调用。

  2. onUpdate: 小部件被添加时 或 更新时 都会调用。

  3. onDeleted: 每删除一次小部件就调用一次。

  4. onDisabled: 当最后一个该类型的小部件被删除时调用。

  5. onReceive: 广播的内置方法,用于分发具体的事件给其他类。根据接收到的Intent中的Action来分别调用onEnabled, onUpdate, onDeleted, onDisabled等方法。

 

RemoteViews的内部机制

RemoteViews的主要功能

构造方法

/**
 * @param packageName 当前应用的包名
 * @param layoutId 待加载的布局文件
*/
public RemoteViews(String packageName, int layoutId);

 

RemoteViews能够支持的View类型有限,也不支持自定义View。现在支持的layout如下:

  • AdapterViewFlipper

  • FrameLayout

  • GridLayout

  • GridView

  • LinearLayout

  • ListView

  • RelativeLayout

  • StackView

  • ViewFlipper

现在支持的widget如下:

  • AnalogClock

  • Button

  • Chronometer

  • ImageButton

  • ImageView

  • ProgressBar

  • TextClock

  • TextView

这些类的子类不被RemoteViews支持。

因为RemoteViews跨进程显示,所以没有提供findViewById方法,无法直接访问里面的View元素,必须通过RemoteViews提供的一系列set方法。大部分set方法都是通过反射来完成的。

 

下面先从理论上分析一下RemoteViews的内部机制

RemoteViews的内部机制概述

以通知栏与桌面小部件为例说明RemomteViews的工作进程。

通知栏和桌面小部件分别由NotificationManagerAppWidgetManager管理,而NotificationManagerAppWidgetManager通过Binder分别与SystemServer进程中的NotificationManagerServiceAppWidgetService进行通信。由此可见,通知栏和桌面小部件中的布局文件实际上是在NotificationManagerServiceAppWidgetService中被加载的,而他们运行在系统的SystemService中,这就和我们的进程构成了跨进程通信的场景。

  1. 在主进程中,创建RemoteViews,由于它实现了Parcelable接口,因此可以通过Binder跨进程传输到SystemServer进程。

  2. SystemServer进程中,系统通过RemoteViews携带的包名属性获取应用资源,并加载RemoteViews携带的布局文件,得到一个View。这样RemoteViews就在SystemServer中完成加载了。

  3. 如果要对RemoteViews进行操作,可以在主进程中调用RemoteViews提供的set方法,系统将每一个View操作对应地封装成一个Action对象,然后通过Binder传输到SystemServer进程中,在RemoteViews中添加一个Action对象,当NotificationManagerAppWidgetManager提交更新时,RemoteViews就执行apply方法来更新View,这会遍历所有暂存的Action对象并调用他们的apply方法来执行具体的View更新操作。

 

通过简单的理论介绍,现在对RemoteView的内部机制有了模糊的理解,接着从代码实现上分析它的工作流程。

RemoteViews的内部机制代码实现

只以一个set方法为例即可,比如setTextViewText方法。

/*
 * @param viewId 被操作的View的Id
 * @param text 要为TextView设置的文本
 */
public void setTextViewText(int viewId, CharSequence text) {
    setCharSequence(viewId, "setText", text);
}

下面看setCharSequence方法,methodName是要调用的方法名,在`setTextViewText方法中就是"setText"。它将set操作的具体实现封装成一个ReflectionAction对象,然后传入addAction方法。

/*
 * @param viewId The id of the view on which to call the method.
 * @param methodName 要调用的方法名
 * @param value The value to pass to the method.
 */
public void setCharSequence(int viewId, String methodName, CharSequence value) {
    addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}

接下来再看addAction的实现

/*
 * @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);
​
    // update the memory usage stats
    a.updateMemoryUsageEstimate(mMemoryUsageCounter);
}

可以看到,RemoteViews有一个实例变量mActions,用于存储Action对象。当RemoteViews调用apply方法时,所有的Action被触发执行。RemoteViewsapply方法如下:

/*
 * @param context Default context to use
 * @param parent Parent that the resulting view hierarchy will be attached to. This method
 * does not attach the hierarchy. The caller should do so when appropriate.
 * @return The inflated view hierarchy
*/
public View apply(Context context, ViewGroup parent) {
    return apply(context, parent, null);
}
​
/** @hide */
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;
}
从代码中可以看出,首先加载布局文件得到View result,然后通过performApply执行一些更新操作。再看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方法。这里就是真正操作View的地方。其他set方法的原理是一样的。

当通知栏和桌面小程序在使用RemoteViews实现它们的工作过程的时候,区别无非就是RemoteViewsapply方法对应到了NotificationManagernotify方法,以及AppWidgetManagerupdateAppWidget方法。这些方法的内部实现也是调用了RemoteViewsapply方法和reapply方法。

apply方法和reapplay方法的区别在于:`apply会加载布局+更新界面,而reapply只更新界面。通知栏和桌面小部件在初始化的时候会调用apply方法,再后续更新的时候则调用reapply方法。

public void reapply(Context context, View v, OnClickHandler handler) {
    RemoteViews rvToApply = getRemoteViewsToApply(context);
​
    if (hasLandscapeAndPortraitLayouts()) {
        if ((Integer) v.getTag(R.id.widget_frame) != rvToApply.getLayoutId()) {
            throw new RuntimeException("Attempting to re-apply RemoteViews to a view that that does not share the same root layout id.");
        }
    }
​
    rvToApply.performApply(v, (ViewGroup) v.getParent(), handler);
    }

接下来看一下封装set方法的ReflectionAction类。

private final class ReflectionAction extends Action {
    ...
    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));//采用反射的方法来更新View
        } catch (ActionException e) {
            throw e;
        } catch (Exception ex) {
            throw new ActionException(ex);
        }
    }
    ...
}

因为更新View时最终要执行Actionapply操作,也就是这里ReflectionActionapply操作。可以看到,这里采用了反射的方法来执行对View的更新操作。

setTextViewText, setBoolean, setLong, setDoubleset方法都使用了ReflectionAction,还有其他Action实现类,比如对应setTextViewSizeTextViewSizeAction,它没有用反射来实现,代码如下:

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

之所以不使用反射来实现TextViewSizeAction,是因为setTextSize这个方法有2个参数,无法复用ReflectionAction

 

参考资料:

1. 《Android开发艺术探索》

2.  https://developer.android.google.cn/reference/android/widget/RemoteViews

你可能感兴趣的:(学习,android)