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對象。之後再解析成對應的佈局對象。然後就可以使用了。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章