《Android 开发艺术探索》笔记7--RemoteViews的内部机制和意义

RemoteViews的内部机制和意义思维导图

RemoteViews的内部机制和意义.png

RemoteViews的内部机制

RemoteViews的作用是在其他进程中显示并更新View界面.

最常用的构造函数就是public RemoteViews(String packageName, int layoutId), 注意RemoteViews目前并不能支持所有的View类型, 目前支持如下(不包括其子类):

Layout

FrameLayout, LinearLayout, RelativeLayout, GridLayout

View

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

RemoteViews没有提供findviewById()方法, 只有一系列的set()方法.

方法名作用
setTextViewText()设置TextView的文本
setTextViewSize()设置TextView的字体大小
setTextColor()设置TextView的字体颜色
setImageViewResource()设置imageView的图片资源
setImageViewBitmap()设置imageView的图片
setInt()反射调用View对象的参数类型为int的方法
setLong()反射调用View对象的参数类型为long的方法
setBoolean()反射调用View对象的参数类型为boolean的方法
setOnClickPendingIntent()为View添加单击事件, 事件类型只能PendingIntent

RemoteViews的工作流程

通知栏和桌面小部件分别由NotificationManagerAppWidgetManager管理, 而这两个管理者都是通过Binder分别和SystemServer进程中的NotificationManagerService以及AppWidgetService进行通信. 由此可见,通知栏和桌面小部件中的布局文件实际上是在NotificationManagerService以及AppWidgetService中被加载的, 而他们运行在系统的SystemServer中, 这就和我们的进程构成了进程间通信.

最开始RemoteViews会通过Binder传递到SystemServer进程, RemoteViews实现了Parcelable接口. 系统根据RemoteViews中的包名等信息去得到该应用的资源, 然后通过LayoutInflate去加载RemoteViews中的布局文件. 在SystemServer进程中加载后的布局文件是一个普通的View, 只不过相对于我们的进程他是一个RemoteViews而已. 接着系统会对View执行一系列界面更新任务, 这些任务就是之前的设置的set(). set方法对View所做的更新不是立即执行, 在RemoteViews内部会记录所有的更新操作, 具体的执行时机要等到RemoteViews被加载以后才能执行, 这样RemoteViews就可以在SystemServer进程中显示, 这就是我们看到的通知栏或者桌面小部件. 当需要更新RemoteViews时, 我们需要调用set方法并通过NotificationManagerAppWidgetManager来提交更新任务, 具体的更新操作也是在SystemServer进程中完成的.

为什么不支持所有的View和其操作? 因为代价太大, View的方法太多, 另外就是大量的IPC操作会影响效率. 为了解决这个问题, 系统并没有通过Binder直接支持View的跨进程访问, 而是提供了一个Action的概念, Action代表一个View操作, Action同样实现了Parcelable接口. 系统首先将View操作封装到Action对象并将这些对象跨进程传输到远程进程, 接着在远程进程中执行Action对象中的具体操作. 在我们的应用中每调用一次set(), RemoteViews中就会添加一个对应的Action对象, 当我们通过NotificationManagerAppWidgetManager来提交我们的更新时, 这些Action对象就会传输到远程进程并在远程进程中一次执行. 如图

[图片上传失败…(image-f43de0-1599738952325)]

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

接下来从源码角度分析.

首先最长用到的setTextViewText(),源码如下


public void setTextViewText(int viewId, CharSequence text) {

   setCharSequence(viewId, "setText", text);

}

接收的参数比较简单,继续跟进setCharSequence()方法.

public void setCharSequence(int viewId, String methodName, CharSequence value) {

   addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));

}

从这里实现看到, 内部并没有对View进程直接的操作, 而是添加一个ReflectionAction()一个看名字类似反射类型的对象. 接下看addAction()

private void addAction(Action a) {

    //省略部分代码...

    if (mActions == null) {

        mActions = new ArrayList<Action>();

    }

    mActions.add(a);

    // update the memory usage stats

    a.updateMemoryUsageEstimate(mMemoryUsageCounter);

}

这里看到, 在RemoteViews内部有一个mActions成员, 它是一个ArrayList, 外界每调用一次set(), RemoteViews就会为其创建一个Action对象并加入到这个集合中, 这里仅仅将Action对象保存了起来, 并未对View进行实际的操作, 这一点在上面的理论分析中已经提到过.

接下来再看ReflectionAction的实现之前, 先看一下RemoteViewsapply()方法以及Action类的实现.

public View apply(Context context, ViewGroup parent, OnClickHandler handler) {

        RemoteViews rvToApply = getRemoteViewsToApply(context);

        View result;

        final Context contextForResources = getContextForResources(context);

        Context inflationContext = new ContextWrapper(context) {

            @Override

            public Resources getResources() {

                return contextForResources.getResources();

            }

            @Override

            public Resources.Theme getTheme() {

                return contextForResources.getTheme();

            }

            @Override

            public String getPackageName() {

                return contextForResources.getPackageName();

            }

        };

        LayoutInflater inflater = (LayoutInflater)

                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        // Clone inflater so we load resources from correct context and

        // we don't add a filter to the static version returned by getSystemService.

        inflater = inflater.cloneInContext(inflationContext);

        inflater.setFilter(this);

        result = inflater.inflate(rvToApply.getLayoutId(), parent, false);

        rvToApply.performApply(result, parent, handler);

        return result;

    }

这段代码首先通过LayoutInflate去加载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()方法, 这里猜想Action对象的apply方法就是真正操作View的地方.

RemoteViews在通知栏和桌面小部件中的工作过程和上面描述的过程是一致的. 当调用了RemoteViews的set方法时, 并不会立刻更新他们的界面, 而必须要通过NotificationManagernotify方法以及AppWidgetManagerupdateAppWidget才能更新他们的界面. 实际上在AppWidgetManagerupdateAppWidget内部实现中, 他们就是通过RemoteViewsapply以及reapply方法来加载或者更新布局的. applyreApply的区别在于:前者会加载布局并更新界面, 而后者只会更新界面. 通知栏和桌面小部件在初始化界面的时候回调用apply()方法, 而在后续的更新界面时则会调用reapply()方法.

了解了apply()以及reapply()的作用后, 接着看Action的子类具体实现, 先看ReflectionAction的具体实现.

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, 还有其他的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 = (TextView) 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;

}

这个类没有使用反射, 因为setTextSize的方法有两个参数,因此无法复用ReflectionAction, 因为这个反射调用只能有一个参数.

关于单击事件, RemoteViews只支持发起PendingIntent,不支持onClickListener()这种模式.

setOnClickPendingIntent,setPendingIntentTemplate,setOnClickFillIntent这三个的区别.

setOnClickPendingIntent: 只支持普通View设置点击事件, 不能给集合(ListView,StackView)中的View设置点击事件,如item. 因为开销比较大, 系统禁止了这种方式. 如果要给集合中的item添加点击事件,则必须使用后两种组合使用才可以.

RemoteViews的意义可以模拟一个通知栏效果并实现跨进程的UI更新

参看文章

《Android 开发艺术探索》书集
《Android 开发艺术探索》 05-理解RemoteViews

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页