《Android 开发艺术探索》笔记7--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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342