024.RemoteViews的内部机制

    RemoteViews 的作用是在其他的进程中显示并且更新View界面,为了更好理解它的内部机制,先看下它的主要功能。
    首先看它最常用的构造方法:
        public RemoteViews( String packageName ,String layoutId ) 
    第一个参数是当前应用的包名,第二个参数表示代价在的布局文件。RemoteViews目前并不能支持所有的View类型,它支持的所有类型如下:
        
    Layout
        FrameLayout 、 LinearLayout 、RelativeLayout 、GridLayout 

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

    上面描述的是RemoteViews所支持的左右的View类型.不支持其他类型和子类。比如我们如果使用EditText 就会抛出异常。RemoteViews没有提供findViewById方法,因此无法直接访问里面的View元素(实际上是因为这个RemoteView不是在这个进程当中,自然不能直接访问这个控件对象的地址了),不过我们可以通过RemoteView的set方法来访问其中的元素,或者是通过反射机制来调用它们的部分方法。
    下面来分析一下RemoteViews的工作过程。Notification 和 AppWidget 分别由NotificationManager和AppWidgetManager 管理,而NotificationManager和AppWidgetManager通过Binder分别和SystemService进程中的NotificationManagerService和AppWidgetService 进行通信。所以,RemoteViews中的layout文件其实是被NotificationManagerService和AppWidgetService加载的,它们运行在SystemServer这个进程当中,所以说我们的进程是在和SystemServer进行跨进程通信。

    首先RemoteViews会通过Binder传递到SystemServer进程,这是因为RemoteViews实现了Parecel接口,所以它这个对象可以被跨进程运输,系统会根据RemoteViews中的包名等信息获取到用户进程的信息来获取资源。之后通过LayoutInflater去加载RemoteViews中的布局文件,在SystemServer进程加载后的布局文件是一个普通的View,只不过对于我们它是一个RemoteViews.之后系统会对View执行一系列界面更新任务,这些任务就是之前我们通过Set方法来提交的。set方法对view所做的更新不是立即生效的,RemoteViews会记录下这些更新操作,等到RemoteViews被加载了以后才会执行,这样RemoteViews就可以在SystemServer进程中显示了,这就是我们所看到的通知栏小戏或者桌面小部件。当需要更新RemoteViews的时候,我们需要调用set方法并且通过NotificationManager和AppWidgetManager来提交到远程进程上,具体的更新操作则是在SystemServer进程中实现的。

    RemoteViews的更新是通过Binder实现的,但是不是直接调用Binder的接口,RemoteViews引入了Action的概念,我们对View的每调用一次set方法都是一个
  Action,这个Action也是实现了Parcelable接口,因此,可以传递给远程进程上。当我们调用了一系列的set方法以后,RemoteViews会产生一组Action,在么我们向NotificationManager或者是AppWigetManager提交了更新之后,这个方法就
会被传递到远程进程上。远程进程再执行这些Action。
    
    下面我们从源码来分析RemoteViews的工作机制:
     /**
     * 相当于调用TextView.setText
     *
     */
    public void setTextViewText(int viewId, CharSequence text) {
        setCharSequence(viewId, "setText", text);
    }

  /**
     * 调用一个Remoteviews上一个控件参数为CharSequence的方法
     */
 public void setCharSequence(int viewId, String methodName, CharSequence value) {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
    }


  
  /**
     * 添加一个Action ,它会在远程进程调用apply方法的时候执行
     *
     * @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<Action>();
        }
        mActions.add(a);

        // update the memory usage stats
        a.updateMemoryUsageEstimate(mMemoryUsageCounter);
    }

上面我们可以看到,在RemoteViews中 有一个叫mActions的列表在维护Action的信息,需要注意的是,这里静静是将Action对象保存了起来了。并未对View进行实际的操作。
    接下来我们看RefletctionAction,可以看到,这个表示的是一个反射动作,通过它对View的操作会以反射的方式来调用,其中getMethod就是根据方法名来获取所需要的Method对象。
   @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);
            }
        }

    接下来我们看RemoteViews的apply方法:
        public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
        RemoteViews rvToApply = getRemoteViewsToApply(context);

        View result;

        Context c = prepareContext(context);

        LayoutInflater inflater = (LayoutInflater)
                c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        inflater = inflater.cloneInContext(c); 
        //设置过滤器,过滤掉一些不满足条件的View,            
     //比如用户自定义的View是不能被解析的,会报错
     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);
            }
        }
    }

上面的实现就是遍历mAction中的Action对象,并且执行它们的apply方法。我们前面看到ReflectionAction的apply就是利用反射机制执行方法,所以,我们可以知道,action#apply其实就是真正执行我们想要的行为的地方。
    RemoteViews在通知栏和桌面小部件中的工作过程和上面描述的过程是一样的,当我们调用RemoteViews的set方法的时候,我们不会更新它们的界面,而是要通过NotificationManager和notify方法和AppWidgetManager的updateAppWidget方法才能更新它们的界面。实际上在AppWigetAManager的updateAppWidget的内部视线中,它们是通过RemoteViews的apply和reapply方法来加载和更新界面的。app会加载并且更新界面,而reapply只会更新界面。通知栏和桌面小插件会在初始化界面的时候调用apply方法,而在后续的更新界面时候会调用reapply方法。
    
    RemoteViews中只支持发起PendingIntent,不支持OnClickListener那种模式,另外,我们需要注意setOnClickPendingIntent、setPendingIntentTemplate以及setOnClickFillIntent它们之前的区别和联系。首先setOnClickPendingIntent用于给普通View设置单击事件,因为开销比较大,所以系统进制了这种方式。其次,如果要给ListView和StackView中的item添加事件,必须要将setPendingIntentTemplate和setOnClickFillIntent组合使用才可以。(使用RemoteViews的setRemoteAdapter 绑定 RemoteViewService)
    
     其他:
 前面是使用系统自带的NotificationManager和AppWidgetManager来使用RemoteViews,那么除了这两种情况,我们就不能使用了吗?肯定不是,我们完全可以自己做NotificationManager和AppWidgetManager一样的工作。我们可以通过AIDL使用Binder来传递RemoteView,也可以通过广播来传递RemoteViews对象。比如我们有2个进程A和B。B可以发送消息给A,然后在A中显示B所需要显示的控件。
   我们可以创建一个RemoteViews对象,然后把它放入Intent当中,这样,在广播接收器我们就能收到这个RemoteViews了。
           不过,我们创建RemoteViews的时候,不能直接使用我们的进程上下文来创建。我们可以查看AppWigetHostView的getDefaultView方法:
        
            
protected View getDefaultView() {
        if (LOGD) {
            Log.d(TAG, "getDefaultView");
        }
        View defaultView = null;
        Exception exception = null;

        try {
            if (mInfo != null) {
                Context theirContext = mContext.createPackageContextAsUser(
                        mInfo.provider.getPackageName(), Context.CONTEXT_RESTRICTED, mUser);
                mRemoteContext = theirContext;
                LayoutInflater inflater = (LayoutInflater)
                        theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                inflater = inflater.cloneInContext(theirContext);
                inflater.setFilter(sInflaterFilter);
                AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
                Bundle options = manager.getAppWidgetOptions(mAppWidgetId);

                int layoutId = mInfo.initialLayout;
                if (options.containsKey(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY)) {
                    int category = options.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY);
                    if (category == AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD) {
                        int kgLayoutId = mInfo.initialKeyguardLayout;
                        // If a default keyguard layout is not specified, use the standard
                        // default layout.
                        layoutId = kgLayoutId == 0 ? layoutId : kgLayoutId;
                    }
                }
                defaultView = inflater.inflate(layoutId, this, false);
            } else {
                Log.w(TAG, "can't inflate defaultView because mInfo is missing");
            }
        } catch (PackageManager.NameNotFoundException e) {
            exception = e;
        } catch (RuntimeException e) {
            exception = e;
        }

        if (exception != null) {
            Log.w(TAG, "Error inflating AppWidget " + mInfo + ": " + exception.toString());
        }

        if (defaultView == null) {
            if (LOGD) Log.d(TAG, "getDefaultView couldn't find any view, so inflating error");
            defaultView = getErrorView();
        }

        return defaultView;
    }
    

由于不在同一个进程中,往往是两个APP,因此资源是不能直接找到的,所以,我们想要通过id ,解析出布局对象,那么就需要我们先获取远程进程的进程上下文,通过Context的createPackageContextAsUser来获取Context对象。之后再解析成对应的布局对象。然后就可以使用了。




猜你喜欢

转载自blog.csdn.net/savelove911/article/details/52459556
024