第五章-RemoteViews內部機制

一、RemoteViews內部機制

RemoteViews的作用在其他進程中顯示並且更新View的界面,爲了更好地理解它的內部機制,我們來看一下他的主要功能。
首先我們看一下他的構造方法,這裏介紹一個最常用的構造方法

public RemoteViews(String packageName, int layoutId) {
	this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
}

他接受兩個參數,第一個表示當前應用的包名,第二個參數表示待加載的佈局文件,這個很好理解,RemoteViews目前並不能支持所有的View類型,支持的類型有:

- Layout

FrameLayout LinearLayout 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,那麼通知欄消息無法將彈出並且會拋出異常。
在這裏插入圖片描述

RemoteViews沒有提供findViewById方法,因此無法直接訪問裏面的View元素,而必須通過RemoteViews所提供的一系列set方法來完成,當然這是因爲RemoteViews在遠程進程中顯示,所以沒辦法直接findViewById。關於set方法,可以看下這表
在這裏插入圖片描述

從這張表可以看出,原本可以直接調用View的方法,現在都需要通過set來完成,而從這些方法的聲明來看,很像是通過反射來完成的,事實大部分set方法的確是通過反射來完成的。

下面描述一下RemoteViews的內部機制,由於RemoteViews主要用於通知欄和桌面小部件中,這裏就通過它們來分析RemoteViews的工作過程。我們知道,通知欄和小部件分別由NotificationManager和AppWidgetProvider管理,**而NotificationManager和AppWidgetProvider通過Binder分別和SystemService進程中的NotificationManagerService以及AppWidgetService進行通信。**由此可見,通知欄和小部件中的佈局文件實際上是在NotificationManagerService以及AppWidgetService中被加載的,而它們運行在系統的SystemServer中,這就和我們的進程構成了跨進程通信的場景。
在這裏插入圖片描述

上面從理論上分析了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 (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);
}

在這裏插入圖片描述

public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
	RemoteViews rvToApply = getRemoteViewsToApply(context);

	View result;
	// RemoteViews may be built by an application installed in another
	// user. So build a context that loads resources from that user but
	// still returns the current users userId so settings like data / time formats
	// are loaded without requiring cross user persmissions.
	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();
		}
	};

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

在這裏插入圖片描述

private void updateNotificationViews(NotificationData.Entry entry,
		StatusBarNotification notification, boolean isHeadsUp) {
	final RemoteViews contentView = notification.getNotification().contentView;
	final RemoteViews bigContentView = isHeadsUp
			? 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);//看到了也是調用了reapply方法來更新界面的
		content = mView;
		recycled = true;
		if (LOGD) Log.d(TAG, "was able to recycle 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;
	}
}

總結下:view的apply方法,最終調用到了action中的apply方法。

瞭解了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的set方法有setTextViewText、setBoolean、setLong等。除了ReflectionAction 還有其它Action,比如TextViewSizeAction,具體實現如下:

private class TextViewSizeAction extends Action {
	public TextViewSizeAction(int viewId, int units, float size) {
		this.viewId = viewId;
		this.units = units;
		this.size = size;
	}

	public TextViewSizeAction(Parcel parcel) {
		viewId = parcel.readInt();
		units = parcel.readInt();
		size  = parcel.readFloat();
	}

	public void writeToParcel(Parcel dest, int flags) {
		dest.writeInt(TEXT_VIEW_SIZE_ACTION_TAG);
		dest.writeInt(viewId);
		dest.writeInt(units);
		dest.writeFloat(size);
	}

	@Override
	public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
		final TextView target = root.findViewById(viewId);
		if (target == null) return;
		target.setTextSize(units, size);
	}

	public String getActionName() {
		return "TextViewSizeAction";
	}

	int units;
	float size;
}

在這裏插入圖片描述
到這裏就基本瞭解了RemoteViews的內部機制

自己總結下:實際就是一個Binder的IPC過程。通過將一系列的操作封裝成Action動作集合,Binder傳輸到遠程進程,遠程進程調用action裏面的apply方法來更新View(裏面可以是反射方法名稱,或者通過特定的Action來實現)。(這樣看起來就像是我們自己更新了一樣)

二、RemoteViews的意義

在這裏插入圖片描述

public class BActivity extends Activity implements View.OnClickListener {

    private Button bt;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_b);
		
		bt = (Button) findViewById(R.id.bt);
        bt.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.bt:
                RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_item);
                remoteViews.setTextViewText(R.id.textView1, "Hello");
                remoteViews.setImageViewResource(R.id.imageview1, R.mipmap.ic_launcher);
                PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, TestActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
                remoteViews.setOnClickPendingIntent(R.id.imageview1, pendingIntent);
                Intent intent = new Intent("action");
				intent.putExtra("remoteViews",remoteViews);
                sendBroadcast(intent);
                break;
        }
    }
}

A的代碼比較簡單只是接收一個廣播就行

public class AActivity extends Activity {

    private LinearLayout mLinearLayout;

    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            RemoteViews remoteViews = intent.getParcelableExtra("remoteViews");
            if (remoteViews != null) {
                updateUI(remoteViews);
            }
        }
    };

    private void updateUI(RemoteViews remoteViews) {
        View view = remoteViews.apply(this, mLinearLayout);
        mLinearLayout.addView(view);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_a);

        initView();
    }

    private void initView() {
        mLinearLayout = (LinearLayout) findViewById(R.id.mLinearLayout);
        IntentFilter intent = new IntentFilter("action");
        registerReceiver(mBroadcastReceiver, intent);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(mBroadcastReceiver);
    }
}

在這裏插入圖片描述

在這裏插入圖片描述
在這裏插入圖片描述

int layoutId = getResources().getIdentifier("layout_simulated_notification","layout",getPackageName());
View view = getLayoutInflater().inflate(layoutId,mLinearLayout,false);
remoteViews.reapply(this,view);
mLinearLayout.addView(view);

到這裏我們對Android的整個View的體系應該有了個比較深入的認識了。

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