第五章 理解RemoteViews

        本章所講述的主題是RemoteViews,從名字可以看出, RemoteViews應該是一種遠程, View,那麼什麼是遠程View呢?如果說遠程服務可能比較好理解,但是遠程View的確沒,聽說過,其實它和遠程Service是一樣的, RemoteViews表示的是一個View結構,它可以,在其他進程中顯示,由於它在其他進程中顯示,爲了能夠更新它的界面, RemoteViews提,供了一組基礎的操作用於跨進程更新它的界面。這聽起來有點神奇,竟然能跨進程更新界,面!但是RemoteViews的確能夠實現這個效果。RemoteViews在Android中的使用場景有,兩種:通知欄和桌面小部件,爲了更好地分析RemoteViews的內部機制,本章先簡單介紹 RemoteViews在通知欄和桌面小部件上的應用,接着分析RemoteViews的內部機制,最後分析RemoteViews的意義並給出一個採用RemoteViews來跨進程更新界面的示例。

5.1 RemoteViews的應用

RemoteViews在實際開發中,主要用在通知欄和桌面小部件的開發過程中。通知欄每,個人都不陌生,主要是通過NotificationManager的notify方法來實現的,它除了默認效果,外,還可以另外定義佈局。桌面小部件則是通過AppWidgetProvider來實現的, AppWidget Provider本質上是一個廣播。通知欄和桌面小部件的開發過程中都會用到RemoteViews,它們在更新界面時無法像在Activity裏面那樣去直接更新View,這是因爲二者的界面都運行,在其他進程中,確切來說是系統的SystemServer進程。爲了跨進程更新界面, RemoteViews提供了一系列set方法,並且這些方法只是View全部方法的子集,另外RemoteViews中所,支持的View類型也是有限的,這一點會在5.2節中進行詳細說明。下面簡單介紹一下第章理解RemoteViews RemoteViews在通知欄和桌面小部件中的使用方法,至於它們更詳細的使用方法請讀者閱,讀相關資料即可,本章的重點是分析RemoteViews的內部機制。

 5.1.1 RemoteViews在通知欄上的應用

首先我們看一下RemoteViews在通知欄上的應用,我們知道,通知欄除了默認的效果,外還支持自定義佈局,下面分別說明這兩種情況。使用系統默認的樣式彈出一個通知是很簡單的,代碼如下:

PendingIntent pendingIntent=PendingIntent.getActivity(this,0,
        new Intent(this,Democtivity_1.class),PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification=new Notification.Builder(this)
        .setSmallIcon(R.mipmap.ic_launcher)
        .setTicker("hello world")
        .setAutoCancel(true)
        .setContentTitle("title")
        .setContentText("describe")
        .setContentIntent(pendingIntent)
        .getNotification();
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(1,notification);
上述代碼會彈出一個系統默認樣式的通知,單擊通知後會打開DemoActivity 1同時會,清除本身。爲了滿足個性化需求,我們還可能會用到自定義通知。自定義通知也很簡單,,首先我們要提供一個佈局文件,然後通過RemoteViews來加載這個佈局文件即可改變通知,的樣式,代碼如下所示。

NotificationCompat.Builder builder=new NotificationCompat.Builder(this)
        .setAutoCancel(true)
        .setSmallIcon(R.mipmap.ic_launcher);

RemoteViews remoteViews=new RemoteViews(getPackageName(),R.layout.layout_refresh_head);
remoteViews.setTextViewText(R.id.tv_refresh,"點擊送美女哦");
remoteViews.setImageViewResource(R.id.iv_refresh_move,R.drawable.ic_loading_gray);
PendingIntent openActivityIntent=PendingIntent.getActivity(this,0,
        new Intent(this,Democtivity_1.class),PendingIntent.FLAG_UPDATE_CURRENT);

builder.setContentIntent(openActivityIntent);
remoteViews.setOnClickPendingIntent(R.id.tv_refresh,openActivityIntent);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(1,builder.build());

 RemoteViews的使用也很簡單,只要提供當前應用的包名和佈局文件的資源id即可創建一個RemoteViews對象。如何更新RemoteViews呢?這一點和更新View有很大的不同,更新RemoteViews時,無法直接訪問裏面的View,而必須通過RemoteViews所提供的一系列方法來更新View。比如設置TextView的文本,要採用如下方式: remoteViews. setTextViewText(R.id.tv_refresh, "點擊送美女哦"),其中setTextViewText的兩個參數分別爲TextView的id和要設置的文本。而設置ImageView的圖片也不能直接訪問ImageView,必須通過如下方式: remoteViews.setlmageViewResource(R.id.iv_refresh_move,R.drawable.ic_loading_gray), setlmageViewResource.的兩個參數分別爲ImageView的id和要設置的圖片資源的id.如果要給一個控件加單擊事,件,則要使用PendingIntent並通過setOnClickPendingIntent方法來實現,比如 remoteViews.setOnClickPendingIntent(R.id.tv_refresh,openActivityIntent)這句代碼會給id爲tv_refresh的View加上單擊事件。關於PendingIntent,它表示的是一種待定的Intent,這個Intent中所包含的意圖必須由用戶來觸發。爲什麼更新RemoteViews如此複雜呢?直觀原因是因爲RemoteViews並沒有提供和View類似的findViewByld這個方法,因此我們無法獲取到RemoteViews中的子View,當然實際原因絕非如此,具體會在 5.2節中進行詳細介紹。

5.1.2 RemoteViews在桌面小部件上的應用

 AppWidgetProvider是Android中提供的用於實現桌面小部件的類,其本質是一個廣播, "即BroadcastReceiver,圖5-2所示的是它的類繼承關係。所以,在實際的使用中,把 AppWidgetProvider當成一個BroadcastReceiver就可以了,這樣許多功能就很好理解了。


爲了更好地展示RemoteViews在桌面小部件上的應用,我們先簡單介紹桌面小部件的開發步驟,分爲如下幾步。

1.定義小部件界面

 , 在res/layout/下新建一個XML文件,命名爲widget.xml,名稱和內容可以自定義,看!這個小部件要做成什麼樣子,內容如下所示。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/iv_img"
        android:src="@drawable/ic_loading_gray"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>

2.定義小部件配置信息

在res/xml/下新建appwidget_provider_info.xml,名稱隨意選擇,添加如下內容:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/widget"
    android:minHeight="84dp"
    android:minWidth="84dp"
    android:updatePeriodMillis="864000"
    >
</appwidget-provider>


上面幾個參數的意義很明確, initialLayout就是指小工具所使用的初始化佈局, minHeight和minWidth定義小工具的最小尺寸, updatePeriodMillis定義小工具的自動更新,週期,毫秒爲單位,每隔一個週期,小工具的自動更新就會觸發。 

3,定義小部件的實現類

這個類需要繼承AppWidgetProvider,代碼如下:

public class MyAppWidgetProvider extends AppWidgetProvider {
    public static final String TAG="MyAppWidgetProvider";
    public static final String CLICK_ACTION="com.yifeng.multiplesku.action.CLICK";
    @Override
    public void onReceive(final Context context, Intent intent) {
        super.onReceive(context, intent);
        Log.i(TAG,"onReceive:action="+intent.getAction());
        //這裏判斷是自己的action,做自己的事情,比如小部件被單擊了要幹什麼,這裏是做一個動畫效果
        if(intent.getAction().equals(CLICK_ACTION)){
            Toast.makeText(context,"clicked it",Toast.LENGTH_SHORT).show();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher);
                    AppWidgetManager appWidgetManager=AppWidgetManager.getInstance(context);
                    for (int i=0;i<37;i++){
                        float degree=(i*10)%360;//0-1
                        RemoteViews remoteViews=new RemoteViews(context.getPackageName(),R.layout.widget);
                        remoteViews.setImageViewBitmap(R.id.iv_img,rotateBitmap(context,bitmap,degree));
                        PendingIntent pendingIntent=PendingIntent.getBroadcast(context,0,
                                new Intent().setAction(CLICK_ACTION),0);
                        remoteViews.setOnClickPendingIntent(R.id.iv_img,pendingIntent);
                        appWidgetManager.updateAppWidget(new ComponentName(context,MyAppWidgetProvider.class),remoteViews);
                        SystemClock.sleep(30);
                    }
                }
            });
        }
    }
    /**
     * 每次桌面小程序更新時都調用一次該方法
     * @param context
     * @param appWidgetManager
     * @param appWidgetIds
     */
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        Log.i(TAG, "onUpdate:");
        int counter = appWidgetIds.length;
        Log.i(TAG, "counter: "+counter);
        for (int i=0;i<counter;i++){
            int appWidgetId = appWidgetIds[i];
            onWidgetUpdate(context,appWidgetManager,appWidgetId);
        }
    }
    /**
     * 桌面小部件更新
     * @param context
     * @param appWidgetManager
     * @param appWidgetId
     */
    private void onWidgetUpdate(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
        Log.i(TAG, "appWidgetId=: "+appWidgetId);
        RemoteViews remoteViews=new RemoteViews(context.getPackageName(),R.layout.widget);
        //桌面小部件   單擊事件發送的intent廣播
        PendingIntent pendingIntent=PendingIntent.getBroadcast(context,0,new Intent().setAction(CLICK_ACTION),0);
        remoteViews.setOnClickPendingIntent(R.id.iv_img,pendingIntent);
        appWidgetManager.updateAppWidget(appWidgetId,remoteViews);
    }
    private Bitmap rotateBitmap(Context context, Bitmap bitmap, float degress){
        Matrix matrix=new Matrix();
        matrix.reset();
        matrix.setRotate(degress);
        return Bitmap.createBitmap(bitmap,0,0,bitmap.getWidth(),bitmap.getHeight(),matrix,true);
    }
}

上面的代碼實現了一個簡單的桌面小部件,在小部件上面顯示一張圖片,單擊它後,這個圖片就會旋轉一週。當小部件被添加到桌面後,會通過RemoteViews來加載佈局文件,而當小部件被單擊後的旋轉效果則是通過不斷地更新RemoteViews來實現的,由此可見, ,桌面小部件不管是初始化界面還是後續的更新界面都必須使用RemoteViews來完成。 

4,在AndroidManifest.xml中聲明小部件

這是最後一步,因爲桌面小部件本質上是一個廣播組件,因此必須要註冊,如下所示。

<receiver android:name=".android_exploit_art.remoteviews.MyAppWidgetProvider">
    <meta-data android:name="android.appwidget.provider"
        android:resource="@xml/appwidget_provider_info">
    </meta-data>
    <intent-filter>
        <action android:name="com.yifeng.multiplesku.action.CLICK"/>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
    </intent-filter>
</receiver>

上面的代碼中有兩個Action,其中第一個Action用於識別小部件的單擊行爲,而第二個Action則作爲小部件的標識而必須存在,這是系統的規範,如果不加,那麼這個receiver就不是一個桌面小部件並且也無法出現在手機的小部件列表裏。 

AppWidgetProvider除了最常用的onUpdate方法,還有其他幾個方法: onEnabled. onDisabled, onDeleted以及onReceive。這些方法會自動地被onReceive方法在合適的時間,調用。確切來說,當廣播到來以後, AppWidgetProvider會自動根據廣播的Action通過 onReceive方法來自動分發廣播,也就是調用上述幾個方法。這幾個方法的調用時機如下所示。

 onEnable:當該窗口小部件第一次添加到桌面時調用該方法,可添加多次但只在第一次調用。 

onUpdate:小部件被添加時或者每次小部件更新時都會調用一次該方法,小部件的,更新時機由

updatePeriodMillis來指定,每個週期小部件都會自動更新一次

onDeleted:每刪除一次桌面小部件就調用一次

onDisabled:當最後一個該類型的桌面小部件被刪除時調用該方法,注意是最後一個

 onReceive:這是廣播的內置方法,用於分發具體的事件給其他方法。關於AppWidgetProvider的onReceive方法的具體分發過程,可以參看源碼中的實現,如下所示。通過下面的代碼可以看出, onReceive中會根據不同的Action來分別調用, onEnable, onDisable和onUpdate等方法

public void onReceive(Context context, Intent intent) {
    // Protect against rogue update broadcasts (not really a security issue,
    // just filter bad broacasts out so subclasses are less likely to crash).
    String action = intent.getAction();
    if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
        Bundle extras = intent.getExtras();
        if (extras != null) {
            int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
            if (appWidgetIds != null && appWidgetIds.length > 0) {
                this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds);
            }
        }
    } else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) {
        Bundle extras = intent.getExtras();
        if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
            final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
            this.onDeleted(context, new int[] { appWidgetId });
        }
    } else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) {
        Bundle extras = intent.getExtras();
        if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)
                && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS)) {
            int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
            Bundle widgetExtras = extras.getBundle(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS);
            this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context),
                    appWidgetId, widgetExtras);
        }
    } else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
        this.onEnabled(context);
    } else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
        this.onDisabled(context);
    } else if (AppWidgetManager.ACTION_APPWIDGET_RESTORED.equals(action)) {
        Bundle extras = intent.getExtras();
        if (extras != null) {
            int[] oldIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS);
            int[] newIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
            if (oldIds != null && oldIds.length > 0) {
                this.onRestored(context, oldIds, newIds);
                this.onUpdate(context, AppWidgetManager.getInstance(context), newIds);
            }
        }
    }
}

上面描述了開發一個桌面小部件的典型過程,例子比較簡單,實際開發中會稍微複雜,一些,但是開發流程是一樣的。可以發現,桌面小部件在界面上的操作都要通過Remote- , Views,不管是小部件的界面初始化還是界面更新都必須依賴它。

5.1.3 PendingIntent概述

在5.1.2節中,我們多次提到PendingIntent,那麼PendingIntent到底是什麼東西呢?它,和Intent的區別是什麼呢?在本節中將介紹Pendinglntent的使用方法。顧名思義, PendingIntent表示一種處於pending狀態的意圖,而pending狀態表示的是,一種待定、等待、即將發生的意思,就是說接下來有一個Intent (即意圖)將在某個待定的,時刻發生。可以看出PendingIntent和Intent的區別在於, PendingIntent是在將來的某個不確定的時刻發生,而Intent是立刻發生。PendingIntent典型的使用場景是給RemoteViews添加單擊事件,因爲RemoteViews運行在遠程進程中,因此RemoteViews不同於普通的View,所以無法直接向View那樣通過setOnClickListener方法來設置單擊事件。要想給 RemoteViews設置單擊事件,就必須使用PendingIntent, PendingIntent通過send和cancel方法來發送和取消特定的待定Intent. Pendinglntent支持三種待定意圖:啓動Activity、啓動Service和發送廣播,對應着它,的三個接口方法,如表5-1所示。


如表5-1所示, getActivity, getService和getBroadcast這三個方法的參數意義都是相同的,第一個和第三個參數比較好理解,這裏主要說下第二個參數requestCode和第四個參數 flags,其中requestCode表示PendingIntent發送方的請求碼,多數情況下設爲0即可,另外, requestCode會影響到flags的效果。flags常見的類型有: FLAG ONE SHOT, FLAG NO CREATE, FLAG CANCEL CURRENT和FLAG UPDATE CURRENT,在說明這四個標,記位之前,必須要明白一個概念,那就是PendingIntent的匹配規則,即在什麼情況下兩個Pendinglntent是相同的。 

PendingIntent的匹配規則爲:如果兩個PendingIntent它們內部的Intent相同並且, requestCode也相同,那麼這兩個PendingIntent就是相同的。requestCode相同比較好理解,那麼什麼情況下Intent相同呢? Intent的匹配規則是:如果兩個Intent的ComponentName和intent-filter都相同,那麼這兩個Intent就是相同的。需要注意的是Extras不參與Intent的匹配過程,只要Intent之間的ComponentName和intent-filter相同,即使它們的Extras不同,那麼這兩個Intent也是相同的。瞭解了PendingIntent的匹配規則後,就可以進一步理解flags參數的含義了,如下所示。

 FLAG ONE SHOT

當前描述的PendingIntent只能被使用一次,然後它就會被自動cancel,如果後續還有相同的PendingIntent,那麼它們的send方法就會調用失敗。對於通知欄消息來說,如果採,用此標記位,那麼同類的通知只能使用一次,後續的通知單擊後將無法打開。

 FLAG NO CREATE

當前描述的PendingIntent不會主動創建,如果當前PendingIntent之前不存在,那麼 getActivity,getService和getBroadcast方法會直接返回null,即獲取PendingIntent失敗。這個標記位很少見,它無法單獨使用,因此在日常開發中它並沒有太多的使用意義,這裏就,不再過多介紹了。 

FLAG CANCEL CURRENT

當前描述的PendingIntent如果已經存在,那麼它們都會被cancel,然後系統會創建一個新的PendingIntent。對於通知欄消息來說,那些被cancel的消息單擊後將無法打開。 

FLAG UPDATE CURRENT

當前描述的PendingIntent如果已經存在,那麼它們都會被更新,即它們的Intent中的 Extras會被替換成最新的,從上面的分析來看還是不太好理解這四個標記位,下面結合通知欄消息再描述一遍。這裏分兩種情況,如下代碼中: manager.notify(1, notification),如果notify的第一個參數id是常量,那麼多次調用notify只能彈出一個通知,後續的通知會把前面的通知完全替代掉,而如果每次id都不同,那麼多次調用notify會彈出多個通知,下面一一說明。

如果notify方法的id是常量,那麼不管PendingIntent是否匹配,後面的通知會直接替,換前面的通知,這個很好理解。

如果notify方法的id每次都不同,那麼當PendingIntent不匹配時,這裏的匹配是指, PendingIntent中的Intent相同並且requestCode相同,在這種情況下不管採用何種標記位,這些通知之間不會相互干擾。如果PendingIntent處於匹配狀態時,這個時候要分情況討論:如果採用了FLAG ONE SHOT標記位,那麼後續通知中的PendingIntent會和第一條通知保持,完全一致,包括其中的Extras,單擊任何一條通知後,剩下的通知均無法再打開,當所有的,通知都被清除後,會再次重複這個過程;如果採用FLAG CANCEL CURRENT標記位,那麼只有最新的通知可以打開,之前彈出的所有通知均無法打開:如果採用 FLAG UPDATE CURRENT標記位,那麼之前彈出的通知中的PendingIntent會被更新,最終它們和最新的一條通知保持完全一致,包括其中的Extras,並且這些通知都是可以打開的。 

5.2 RemoteViews的內部機制

RemoteViews的作用是在其他進程中顯示並更新View界面,爲了更好地理解它的內部機制,我們先來看一下它的主要功能。首先看一下它的構造方法,這裏只介紹一個最常用的構造方法: public RemoteViews(String packageName, int layoutld),它接受兩個參數,第一個表示當前應用的包名,第二個參數表示待加載的佈局文件,這個很好理解。RemoteViews目前並不能支持所有的View類型,它所支持的所有類型如下:

 Layout 

FrameLayout. LinearLayout. RelativeLayout. GridLayout. 

View 

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

上面所描述的是RemoteViews所支持的所有的View類型, RemoteViews不支持它們的,子類以及其他View類型,也就是說RemoteViews中不能使用除了上述列表中以外的View,也無法使用自定義View,比如如果我們在通知欄的RemoteViews中使用系統的EditText,那麼通知欄消息將無法彈出並且會拋出如下異常:


上面的異常信息很明確, android.widget.EditText不允許在RemoteViews中使用

RemoteViews沒有提供findViewByld方法,因此無法直接訪問裏面的View元素,而必須通過RemoteViews所提供的一系列set方法來完成,當然這是因爲RemoteViews在遠程進程中顯示,所以沒辦法直接findViewByld。表5-2列舉了部分常用的set方法,更多的方法請查看相關資料。


從表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系統在這方面的設計的確很精妙。

上面從理論上分析了RemoteViews的內部機制,接下來我們從源碼的角度再來分析, RemoteViews的工作流程。它的構造方法就不用多說了,這裏我們首先看一下它提供的一系列set方法,比如setTextViewText方法,其源碼如下所示。

public void setTextViewText(int viewId, CharSequence text) {
    setCharSequence(viewId, "setText", text);
}

在上面的代碼中, viewld是被操作的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));
}
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成員,它是一個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 = inflateView(context, rvToApply, parent);
    loadTransitionOverride(context, handler);

    rvToApply.performApply(result, parent, handler);

    return result;
}

        從上面代碼可以看出,首先會通過LayoutInflater去加載RemoteViews中的佈局文件, , RemoteViews中的佈局文件可以通過getLayoutld這個方法獲得,加載完佈局文件後會通過, 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方法時,並不會立刻更新它們的界面,而必須要通過Notification Manager的notify方法以及AppWidgetManager的updateAppWidget才能更新它們的界面。實際上在AppWidgetManager的updateAppWidget的內部實現中,它們的確是通過 RemoteViews的apply以及reapply方法來加載或者更新界面的, apply和reApply的區別在於: apply會加載佈局並更新界面,而reApply則只會更新界面。通知欄和桌面小插件在初始化界面時會調用apply方法,而在後續的更新界面時則會調用reapply方法。這裏先看一下BaseStatusBar的updateNotificationViews方法中,如下所示。



很顯然,上述代碼表示當通知欄界面需要更新時,它會通過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 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;
    }
}

從上述代碼可以發現,桌面小部件在更新界面時也是通過RemoteViews的reapply方法來實現的。

瞭解了apply以及reapply的作用以後,我們再繼續看一些Action的子類的具體實現, 首先看一下ReflectionAction的具體實現,它的源碼如下所示。


        通過上述代碼可以發現, ReflectionAction表示的是一個反射動作,通過它對View的!操作會以反射的方式來調用,其中getMethod就是根據方法名來得到反射所需的Method對 ,象。使用ReflectionAction的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;
    }

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

    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(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;

    public final static int TAG = 13;
}

         TextViewSizeAction的實現比較簡單,它之所以不用反射來實現,是因爲setTextSize這個方法有2個參數,因此無法複用ReflectionAction,因爲ReflectionAction的反射調用只有一個參數。其他Action這裏就不一一進行分析了,讀者可以查看RemoteViews的源代碼。

        關於單擊事件, RemoteViews中只支持發起Pendinglntent,不支持onClickListener那,種模式。另外,我們需要注意setOnClickPendingIntent, setPendingIntentTemplate以及 setOnClickFillInIntent它們之間的區別和聯繫。首先setOnClickPendingIntent用於給普通 View設置單擊事件,但是不能給集合(ListView和StackView)中的View設置單擊事件,比如我們不能給ListView中的item通過setOnClickPendinglntent這種方式添加單擊事件,因爲開銷比較大,所以系統禁止了這種方式;其次,如果要給ListView和StackView中的, item添加單擊事件,則必須將setPendingIntentTemplate和setOnClickFillInIntent組合使用纔可以。

5.3  RemoteViews的意義

        在5.2節中我們分析了RemoteViews的內部機制, 瞭解RemoteViews的內部機制可以讓我們更加清楚通知欄和桌面小工具的底層實現原理,但是本章對RemoteViews的探索並,沒有停止,在本節中,我們將打造一個模擬的通知欄效果並實現跨進程的UI更新。

        首先有2個Activity分別運行在不同的進程中,一個名字叫A,另一個叫B,其中A扮演着模擬通知欄的角色,而B則可以不停地發送通知欄消息,當然這是模擬的消息。爲了模擬通知欄的效果,我們修改A的process屬性使其運行在單獨的進程中,這樣A和B就構成了多進程通信的情形。我們在B中創建RemoteViews對象,然後通知A顯示這個RemoteViews對象。如何通知A顯示B中的RemoteViews呢?我們可以像系統一樣採用Binder來實現,但是這裏爲了簡單起見就採用了廣播。B每發送一次模擬通知,就會發送一個特定的廣播,然後A接收到廣播後就開始顯示B中定義的RemoteViews對象,這個過,程和系統的通知欄消息的顯示過程幾乎一致,或者說這裏就是複製了通知欄的顯示過程而已。首先看B的實現, B只要構造RemoteViews對象並將其傳輸給A即可,這一過程通知欄是採用Binder實現的,但是本例中採用廣播來實現, RemoteViews對象通過Intent傳輸到A中,代碼如下所示。






        上述代碼很簡單,除了註冊和解除廣播以外,最主要的邏輯其實就是updateU1方法。當A收到廣播後,會從Intent中取出RemoteViews對象,然後通過它的apply方法加載佈局文件並執行更新操作,最後將得到的View添加到A的佈局中即可。可以發現,這個過,程很簡單,但是通知欄的底層就是這麼實現的。

        本節這個例子是可以在實際中使用的,比如現在有兩個應用,一個應用需要能夠更新,另一個應用中的某個界面,這個時候我們當然可以選擇AIDL去實現,但是如果對界面的,更新比較頻繁,這個時候就會有效率問題,同時AIDL接口就有可能會變得很複雜。這個 · 時候如果採用RemoteViews來實現就沒有這個問題了,當然RemoteViews也有缺點,那就,是它僅支持一些常見的View,對於自定義View它是不支持的。面對這種問題,到底是採,用AIDL還是採用RemoteViews,這個要看具體情況,如果界面中的View都是一些簡單的,且被RemoteViews支持的View,那麼可以考慮採用RemoteViews,否則就不適合用, RemoteViews了。

        如果打算採用RemoteViews來實現兩個應用之間的界面更新,那麼這裏還有一個問題,那就是佈局文件的加載問題。在上面的代碼中,我們直接通過RemoteViews的apply方法來加載並更新界面,如下所示。


這種寫法在同一個應用的多進程情形下是適用的,但是如果A和B屬於不同應用,那麼B中的佈局文件的資源id傳輸到A中以後很有可能是無效的,因爲A中的這個佈局文,件的資源id不可能剛好和B中的資源id一樣,面對這種情況,我們就要適當修改, RemoteViews的顯示過程的代碼了。這裏給出一種方法,既然資源id不相同,那我們就通,過資源名稱來加載佈局文件。首先兩個應用要提前約定好RemoteViews中的佈局文件的資,源名稱,比如"layout simulated notification",然後在A中根據名稱查找到對應的佈局文!件並加載,接着再調用RemoteViews的reapply方法即可將B中對View所做的一系列更新操作全部作用到A中加載的View上面。關於apply和reapply方法的差別在前面已經提到過,這裏就不多說了,這樣整個跨應用更新界面的流程就走通了,具體效果如圖5-4所示。可以發現B中的佈局文件已經成功地在A中顯示了出來。修改後的代碼如下:



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