RemoteViews的内部机制

5.2 RemoteViews的内部机制

RemoteViews的作用是在其他进程中显示并更新View界面,为了更好地理解它的内部机制,我们先来看一下它的主要功能。首先看一下它的构造方法,这里只介绍一个最常用的构造方法:public RemoteViews(String packageName, int layoutId),它接受两个参数,第一个表示当前应用的包名,第二个参数表示待加载的布局文件,这个很好理解。RemoteViews目前并不能支持所有的View类型,它所支持的所有类型如下:

Layout

FrameLayout、LinarLayout、RelativeLayout、GridLayout。

View

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

上面所描述的是RemoteViews所支持的所有的View类型,RemoteViews不支持它们的子类以及其他View类型,也就是说RemoteViews中不能使用除了上述列表意外的View,也无法只用自定义View。比如如果我们在通知栏的RemoteViews中使用系统的EditText,那么通知栏消息将无法弹出并且会抛出如下异常。

E/StatusBar(765): couldn't inflate view for notification com.chenstyle.chapter_5/0x2
E/StatusBar(765): android.view.InflateException: Binary XML file line #25: Error inflating class android.widget.EditText
E/StatusBar(765): Caused by: android.view.InflatedException: Binary XML file line #25: Class not allowed to be inflated android.widget.EditText
E/StatusBar(765):   at android.view.LayoutInflater.failNotAllowed(LayoutInflater.java:695)
E/StatusBar(765):   at android.view.LayoutInflater.createView(LayoutInflater.java:628)
E/StatusBar(765):   ...21 more

上面的异常信息很明确,android.widget.EditText不允许在RemoteViews中使用。

RemoteViews没有提供findViewById方法,因此无法直接访问里面的View元素,而必须通过RemoteViews所提供的一系列set方法来完成,当然这是因为RemoteViews在远程进程中显示,所以没办法直接findViewById。表5-2列举了部分常用的set方法,更多方法请查看相关资料。

表5-2 RemoteViews的部分set方法

方法名 作用
setTextViewText(int viewId, CharSquence text) 设置TextView的文本
setTextViewTextSize(int viewId, int units, float size) 设置TextView的字体大小
setTextColor(int viewId, int color) 设置TextView的字体颜色
setImageViewResource(int viewId, int srcId) 设置ImageView的图片资源
setImageViewResource 设置ImageView的图片
setInt(int viewId, String methodName, int value) 反射调用View对象的参数类型为int的方法
setLong(int viewId, String methodName, long value) 反射调用View对象的参数类型为long的方法
setBoolean(int viewId, String methodName, boolean value) 反射调用View的对象的参数为boolean的方法
setOnClickPendingIntent(int viewId, PendingIntent pendingIntent) 为View添加单击事件,事件类型只能为PendingIntent

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

下面描述一下RemoteViews的内部机制,由于RemoteViews主要用于通知栏和桌面小部件之中,这里就通过它们来分析RemoteViews的工作过程。我们知道,通知栏和桌面小部件分别由NotificationManager和AppWidgetManager管理,而NotificationManager和AppWidgetManager通过Binder分别和SystemServer进程中的NotificationManagerService以及AppWidgetService进行通信。由此可见,通知栏和桌面小部件中的布局文件实际上是在NotificationManagerService以及AppWidgetService中被加载的,而他们运行在系统的SystemServer中,这就和我们的进程构成了跨进程的通信场景。

首先RemoteViews会通过Binder传递到SystemServer进程,这是因为RemoteViews实现了Parcelable接口,因此它可以跨进程传输,系统会根据RemoteViews中的包名等信息去得到该应用的资源。然后会通过LayoutInflater去加载RemoteViews中的布局文件。在SystemServer进程中加载后的布局文件是一个普通的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对象中的具体操作。在我们的应用中每调用一次set方法,RemoteViews中就会添加一个对应的Action对象,当我们通过NotificationManager和AppWidgetManager来提交我们的更新时,这些Action对象就会传输到远程进程并在远程进程中依次执行,这个过程可以参看图5-3。远程进程通过RemoteViews的apply方法来进行View的更新操作,RemoteViews的apply方法内部则会去遍历所有的Action对象并调它们的apply方法,具体的View更新操作是由Action对象的apply方法来完成的。上述做法的好处是显而易见的,首先不需要定义大量的Binder接口,其次通过在远程进程中批量执行RemoteViews的修改操作从而避免了大量的IPC操作,这就提高了程序的性能,由此可见,Android系统在这方面的设计的确很精妙。

图5-3 RemoteViews的内部机制.jpg

上面从理论上分析了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 (mAction == null) {
        mAction = new ArrayList<Action>();
    }
    mAction.add(a);
    // update the memory usage stats
    a.updateMemoryUsageEstimate(mMemoryUsageCounter);
}

从上述代码可以知道,RemoteViews内部有一个mActions成员,它是一个ArrayList,外界每调用一次set方法,RemoteViews就会为其创建一个Action对象并加入到这个ArrayList中。需要注意的是,这里仅仅是将Action对象保存起来了,并未对View进行实际的操作,这一点在上面的理论分析中已经提到过了。到这里setTextViewText这个方法的源码已经分析完了,但是我们好像还是什么都不知道的感觉,没关系,接着我们需要看一下这个ReflectionAction的实现就知道了。在看它的实现之前,我们需要先看一下RemoteViews的apply方法以及Action类的实现,首先看一下RemoteViews的apply方法,如下所示。

public View apply(Context context, ViewGroup parent, onClickHandler handler) {
    RemoteViews rvToApply getRemoteViewsToApply(context);
    
    View result;
    ...
    
    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;
}

从上面的代码可以看出,首先会通过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);
        }
    }
}

performApply的实现就比较好理解了,它的作用就是遍历mActions这个列表并执行每个Action对象的apply方法。还记得mAction吗?每一次的set操作都会对应着它里面的一个Action对象,因此我们可以断定,Action对象的apply方法就是真正操作View的地方,实际上的确如此。

RemoteViews在通知栏和桌面小部件中的工作过程和上面描述的过程是一致的,当我们调用RemoteViews的set方法时,并不会立刻更新它们的界面,而必须要通过NotificationManager的notify方法以及AppWidgetManager的updateAppWidget的内部实现中,它们的确是通过RemoteViews的apply以及reapply方法来加载或者更新界面的,apply和reApply的区别在于:apply会加载布局并更新界面,而reApply则只会更新界面。通知栏和桌面小插件在初始化界面时会调用apply方法,而在后续的更新界面时则会调用reapply方法。这里先看一下BaseStatusBar的updateNotificationViews方法中,如下所示。

private void updateNotificationViews(NotificationData.Entry entry, StatusBarNotification notification, boolean isHandsUp) {
    final RemoteViews contentViews = notification.getNotification().contentView;
    final RemoteViews bigContentView = isHandsUp ? notification.getNotification().headsUpContentView : notification.getNotification().bigCOntentView;
    final Notification publicVersion = notification.getNotification().publicVersion;
    final RemoteViews publicContentView = publicVersion != null ? publicVersion.contentView : null;
    
    // Reapply the RemoteViews
    contentView.reapply(mContext, entry.expanded, mOnClickHandler);
    ...
}

很显然,上述代码表示当通知栏界面需要更新时,它会通过RemoteViews的reapply方法来更新界面。

接着再看一下AppWidgetHostView的updateAppWidget方法,在它的内部有如下一段代码:

mRemoteContext = getRemoteContext();
int layoutId = remoteViews.getLayoutId();

// If our stale view has been prepared to match active, and the new
// layout matches, try recycling it
if (content == null && layoutId == mLayoutId) {
    try {
        remoteViews.reapply(mContext, mView, mOnClickHandler);
        content = mView;
        recycled = true;
        if (LOGD) Log.d(TAG, "was able to recycled existing layout");
    } catch (RuntimeException e) {
        exception = e;
    }
}

// Try normal RemoteView inflation
if (content == null) {
    try {
        content = remoteViews.apply(mContext, this, mOnClickHandler);
        if (LOGD) Log.d(TAG, "had to inflate new layout");
    } catch (RuntimeException e) {
        exception = e;
    }
}

从上述代码可以发现,桌面小部件在更新界面时也是通过RemoteViews的reapply方法来实现的。

了解了apply以及reapply的作用以后,我们再继续看一些Action的子类的具体实现,首先看一下RefectionAction的具体实现,它的源码如下所示。

private final class ReflectionAction extends Action {
    ReflectionAction(int viewId, String methodName, int type, Object value) {
        this.valueId = 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);
        }
    }
}

通过上述代码可以发现,RefectionAction表示的是一个反射动作,通过它对View的操作会以反射的方式来调用,其中getMethod就是根据方法名来得到反射所需要的Method对象。使用Reflection的set方法有:setTextViewText、setBoolean、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 = (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;
}

TextViewSizeAction的实现比较简单,它之所以不用反射来实现,是因为setTextSize这个方法有2个参数,因此无法复用ReflectionAction,因为ReflectionAction的反射调用只有一个参数。其他Action这里就不一一进行分析了,读者可以查看RemoteViews的源代码。

关于单击事件,RemoteViews中只支持发起PendingIntent,不支持onClickListener那种模式。另外,我们需要注意setOnClickPendingIntent、setPendingIntentTemplate以及setOnClickFillInIntent它们之间的区别和联系。首先setOnClickPendingIntent用于给普通View设置单击事件,但是不能给集合(ListView和StackView)中的View设置单击事件,因为开销比较大,所以系统禁止了这种方式;其次,如果要给ListView和StackView中的item添加单击事件,则必须将setPendingIntentTemplate和setOnClickFillInIntent组合使用才可以。

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

推荐阅读更多精彩内容