您的位置:首页 > 其它

RemoteViews之RemoteViews的内部机制

2016-05-19 17:06 337 查看
RemoteViews的作用是在其他的进程显示并更新View界面的

RemoteViews的内部机制

RemoteViews支持的layout和View:

layout:
FrameLayout  LinearLayout  RelativeLayout  GridLayout
View:
AnalogClock button Chronometer ImageButton ImageView ProgressBar TextView ViewFlipper ListView GridView StackView AdapterViewFlipper ViewStub


上述就是RemoteViews所支持的所有layout和View 如果使用了这些之外的View 将会抛出异常 表明不可使用该View

RemoteViews设置view内容的方法:

由于RemoteViews没有findViewById的方法  因为它是远程的View即使findViewById我们也不知道远程app的资源文件id  所以如果想要更新View的内容 就要使用RemoteViews提供的一系列set方法  如下


方法名作用
setTextViewText(int viewId,CharSequence text)设置TextView的文本内容 第一个参数是TextView的id 第二个参数是设置的内容
setTextViewTextSize(int viewId,int units,float size)设置TextView的字体大小 第二个参数是字体的单位
setTextColor(int viewId,int color)设置TextView字体颜色
setImageViewResource(int viewId,int srcId)设置ImageView的图片
setInt(int viewId,String methodName,int value)反射调用View对象的参数类型为Int的方法 比如上述的setImageViewResource的方法内部就是这个方法实现 因为srcId为int型参数
setLong setBoolean类似于setInt
setOnClickPendingIntent(int viewId,PendingIntent pendingIntent)添加点击事件的方法
上述是常用的一些RemoteViews的方法 还有很多没有列举 但是都是差不多的调用方式 大部分是通过反射调用View的方法

这个过程大体是这个样子的 首先RemoteViews每调用一个set方法都会向自身添加一个Action Action中封装了一些处理布局的方法 也是序列化对象

之后当RemoteViews需要加载的时候会调用自身的apply方法 这个方法会遍历所有的action一个一个执行action的apply方法 在action的apply方法中会反射调用View的方法 从而实现布局的更新

首先我们要知道这是进程间通信 所以一般是基于Binder实现的 在使用RemoteViews的时候 它会通过Binder传递到SystemServer进程 因为RemoteViews实现了Parcelable接口 所以它可以跨进程传输(跨进程传输其实就是序列化和反序列化的过程。。。) 系统会根据RemoteViews中的包名等信息去获取该应用中的资源 然后通过LayoutInFlater加载RemoteViews中的布局 显示出来 然后还会调用RemoteViews中的action的apply方法 更新布局的信息

理论上Binder可以支持View的所有操作 但是那样太麻烦了 需要提供很多Ipc操作 效率势必会降低 所以没有那样实现 所以就使用了Action 在我们通过NotificationManager或者 AppWidgetManager提交更新时 就会一次执行Action

下面我们从源码的角度看看这个过程的实现:

我们分析AppWidget中 更新TextView的方法 setTextViewText的使用过程

RemoteViews remoteViews = new RemoteViews(context.getPackageName(),R.layout.my_widght); //创建一个RemoteViews布局
remoteViews.setTextViewText(R.id.txt_1, "哈哈哈哈"); //设置远程布局中的TextViewText
appWidgetManager.updateAppWidget(new ComponentName(context,AppWidgetProvider.class),remoteViews); //提交更新


在RemoteViews执行每个set方法的时候 都会向RemoteViews中添加一个Action :

public void setTextViewText(int viewId, CharSequence text) {  //setTextViewText基于setCharSequence反射实现
setCharSequence(viewId, "setText", text);
}

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

private void addAction(Action a) { //添加Action的方法
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);  //添加action
// update the memory usage stats
a.updateMemoryUsageEstimate(mMemoryUsageCounter);
}


上述代码就向RemoteViews中添加了一个ReflectionAction 它是Action的子类 是用反射调用方法的Action 其中还有TextViewSizeAction、ViewPaddingAction、SetOnClickPendingIntent等Action 我们暂且只分析 ReflectionAction

经过上面的代码分析 我们已经执行了setTextViewText方法 并在RemoteViews中添加了一个ReflectionAction 对象 下一步就是AppWidgetManager提交更新了 提交更新之后RemoteViews便会由Binder跨进程传输到SystemServer进程中

之后在这个进程 RemoteViews会执行它的apply方法或者reapply方法

/** @hide */
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context); //获得RemoteViews对象  有 横屏和竖屏两种 选择

View result;
//.......此处省略一部分代码

LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);  //获得inflater 布局加载器

// 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);  //分发请求 方法中遍历Action   下面有此方法实现

return result;
}


在RemoteViews的apply方法中 主要执行了 这几步

1.获取remoteViews对象

2.获取Inflater布局加载器 服务

3.根据remoteViews对象中的LayoutId和布局加载器 加载布局View —-result

4.执行remoteViews的performApply方法 遍历Action

其中performApply方法的参数中有个parent参数 先前一直不理解 查看注释 Parent that the resulting view hierarchy will be attached to. This method does not attach the hierarchy.

意思也就是 返回的结果的View所依附的父布局 我猜测也就是SysTemServer的那个布局 比如桌面控件 那就是桌面 通知栏 的话 就是通知栏。。 也不知道对不对。。。如果有哪位大哥知道它真正的含义 可以给我留言 小弟万分感谢

对于 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
Action a = mActions.get(i);
a.apply(v, parent, handler);
}
}
}


这个方法 非常简单 就是把remoteViews中的Action全部取出来 然后遍历执行action的apply方法

需要看的只有action的apply方法了

@Override
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) { //这是ReflectionAction的apply方法
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);
}
}


参数root也就是remoteViews的整体布局 viewId也就是需要更新的子控件 比如如果是setTextViewText 那就是某个Textview了

然后就是根据反射调用TextView的setText方法了 就完成了布局的更新

需要注意的是 这个ReflectionAction 只适用于一个参数的方法 比如setText(String text) 如果是有2个参数的方法就不可以了 比如 setTextSize(int units ,int size) 这时候可以使用TextViewSizeAction实现 对于其他的Action在这我就不一一分析了 大家可以查看RemoteViews的源码 Action都是它的内部类 我们也无法在外部使用

对于单击事件 RemoteViews只支持发起PendingIntent 不支持onclickListener模式 我们还要区分开setOnclickPendingIntent setPendingIntentTemplate setOnClickFillInIntent他们之间的区别和联系

setOnclickPendingIntent 不可以给集合类的控件添加单击事件 因为消耗太大 如果需要给集合类的控件添加 点击事件 需要 setPendingIntentTemplate 和setOnClickFillInIntent配合使用 具体怎么配合可以百度。。我还没用到过。。

RemoteViews的意义

上面我们分析了RemoteViews的内部机制 我们就可以更加灵活的使用它进行一些远程的控件更新了 比如我们需要实现两个进程间的控件远程信息更新 在以往 我们需要用AIDL 或者其他IPC方式 但是我们了解RemoteViews之后完全可以使用它来实现

比如说 我们想实现 一个 2个进程间模拟通知栏 其实很简单 原理如下:

1.创建两个Activity 在2个进程运行 (这里之所以在一个程序中创建2个进程是为了方便。。)

2.在一个Activity中发送一个广播 在广播中put一个remoteViews 因为它是Pracelable对象

3.在另一个Activity中接受这个广播 然后取出remoteViews对象 执行它的apply方法 之后把view添加到这个Activity的父布局中即可

但是上面其实还存在一个问题 那就是如果在两个不同的app中 对于remoteViews传过来的viewId 另一个app中可能没有对应的 id 而为什么在AppWidget中可以呢 我猜测它是在apply之前在app中找到了那个资源 然后加载就没问题了

而我们这 我们需要自行处理一下 我们需要在两个app中事先规定好资源的名字 一致 然后通过名字查找资源的id 然后调用reapply方法加载布局 具体实现如下 :

int layoutId =  getResources().getIdentifier("layout_name","layout",getPackageName());//此方法通过方法寻找资源id
View view = getLayoutInflater().inflate(layoutId,parentView,false);
remoteViews.reapply(this,view);//此处参数和apply不同
parentView.addView(view);


这样就实现了

但是我还有个疑问 reapply不是只会更新界面吗 没有加载界面会不会出错??

这个疑问在晚上。。回来再看代码的时候解开了。。。 原来很简单。。

apply和reapply的区别是什么呢 apply相对于reapply来说 它多了加载布局这一步 我们先来对比一下 reapply和apply的代码

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


reapply:

public void reapply(Context context, View v, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context);

// In the case that a view has this RemoteViews applied in one orientation, is persisted
// across orientation change, and has the RemoteViews re-applied in the new orientation,
// we throw an exception, since the layouts may be completely unrelated.
//.....此处省略部分代码
rvToApply.performApply(v, (ViewGroup) v.getParent(), handler);
}


我们可以看的出来 不同的就是 apply多了下面这部分代码

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


然后最后的performApply的参数不一样

先看多的这一部分代码 就是通过服务获取布局加载器 然后加载RemoteViews的布局罢了 最后获取这个布局 result 而reapply没有这一步 因为reapply使用的场合通常都是布局已经加载好了 不需要重新加载了

然后看最后的performApply函数

//reapply
rvToApply.performApply(v, (ViewGroup) v.getParent(), handler);

//apply
rvToApply.performApply(result, parent, handler);


不同的是父布局的获取不同 一个是getParent方法获取,一个是设置

到这里你可能会问 reapply的父布局如果没有怎么办 那岂不是v.getParent()方法会返回空吗 ,这种情况是可以避免出现的,因为reapply在源码中调用通常会调用在apply之后 也就是布局已经加载完成了 父布局也已经指定了,而如果我们自己写代码 就比如上面我们写的那个代码 我们在reapply外加载布局的时候 也指定了布局的父容器

View view = getLayoutInflater().inflate(layoutId,parentView,false);


parentView就是父容器 所以不需要担心这一点 好了RemoteViews的使用的介绍也差不多完成了

最后它和AIDL的使用时机怎么区别我说一下

如果说两个进程间的布局更新 都是些简单的View 最好使用RemoteViews 因为它内部有Action模式比AIDL效率高 如果布局有自定义的View 那就只能使用AIDL 通信 然后更新布局了
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: