天天看點

Android學習--深入探索RemoteViews什麼是RemoteViewsRemoteViews的應用場景RemoteViews的使用RemoteViews的内部機制RemoteViews的意義感謝

版權聲明:本文為部落客原創文章,轉載請注明出處http://blog.csdn.net/u013132758。 https://blog.csdn.net/u013132758/article/details/81435591

什麼是RemoteViews

RemoteViews表示的是一個View結構,它可以在其他程序中顯示,由于它在其他程序中顯示,為了能夠及時更新它的界面,RemoteViews提供了一組基礎的操作來跨程序更新它的界面。源碼中對于它的解釋如下:

/**
 * A class that describes a view hierarchy that can be displayed in
 * another process. The hierarchy is inflated from a layout resource
 * file, and this class provides some basic operations for modifying
 * the content of the inflated hierarchy.
 */           

RemoteViews

相信很多人跟我一樣覺得這可能是一個View或是layout。真的是這樣嗎?其實不然上面描述中說到它是描述一個View結構,并不是一個View,下面我們來通過源碼看看

RemoteViews

public class RemoteViews implements Parcelable, Filter {
......
}           

從它的繼承方式來看,它跟View和Layout并沒有什麼關系。下面我們來看看

RemoteViews

如何使用。

RemoteViews的應用場景

1、應用于通知欄

2、應用于桌面小部件

RemoteViews的使用

前面說了

RemoteViews

用于通知欄和桌面小部件,下面我們一個個來看

RemoteViews

是怎麼使用的。

RemoteViews在通知欄中使用

RemoteViews

在通知欄中的應用還是比較簡單的,話不多說我們直接撸代碼

Notification notification = new Notification();
    notification.icon = R.mipmap.ic_launcher;
    notification.tickerText = "hello notification";
    notification.when = System.currentTimeMillis();
    notification.flags = Notification.FLAG_AUTO_CANCEL;
    Intent intent = new Intent(this, RemoteViewsActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);//RemoveViews所加載的布局檔案
    remoteViews.setTextViewText(R.id.tv, "RemoteViews應用于通知欄");//設定文本内容
    remoteViews.setTextColor(R.id.tv, Color.parseColor("#abcdef"));//設定文本顔色
    remoteViews.setImageViewResource(R.id.iv, R.mipmap.ic_launcher);//設定圖檔
    PendingIntent openActivity2Pending = PendingIntent.getActivity
            (this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);//設定RemoveViews點選後啟動界面
    remoteViews.setOnClickPendingIntent(R.id.tv, openActivity2Pending);
    notification.contentView = remoteViews;
    notification.contentIntent = pendingIntent;
    NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    manager.notify(2, notification);           

RemoteViews在桌面小部件中的應用

桌面小部件是通過

AppWidgetProVider

來實作的,而

AppWidgetProVider

繼承自BroadcastReceiver,是以可以說

AppWidgetProVider

是個廣播。

1、定義小部件的界面

首先,我們需要在xml檔案中定義好桌面小部件的界面。

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

    <ImageView
        android:id="@+id/iv"
        android:layout_width="360dp"
        android:layout_height="360dp"
        android:layout_gravity="center" />
</LinearLayout>
           

2、定義小部件的配置資訊

在res/xml/下建立一個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="360dp"
      android:minWidth="360dp"
      android:updatePeriodMillis="864000"/>           

3、定義小部件的實作類

這個類需要繼承

AppWidgetProVider

,我們這裡實作一個簡單的widget,點選它後,3張圖檔随機切換顯示。

public class ImgAppWidgetProvider extends AppWidgetProvider {
    public static final String TAG = "ImgAppWidgetProvider";
    public static final String CLICK_ACTION = "packagename.action.click";
    private static int index;

    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);
        if (intent.getAction().equals(CLICK_ACTION)) {
            RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);

            updateView(context, remoteViews, appWidgetManager);
        }
    }

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);

        updateView(context, remoteViews, appWidgetManager);
    }

    // 由于onReceive 和 onUpdate中部分代碼相同 則抽成一個公用方法
    public void updateView(Context context, RemoteViews remoteViews, AppWidgetManager appWidgetManager) {
        index = (int) (Math.random() * 3);
        if (index == 1) {
            remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei1);
        } else if (index == 2) {
            remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei2);
        } else {
            remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei3);
        }
        Intent clickIntent = new Intent();
        clickIntent.setAction(CLICK_ACTION);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, clickIntent, 0);
        remoteViews.setOnClickPendingIntent(R.id.iv, pendingIntent);
        appWidgetManager.updateAppWidget(new ComponentName(context, ImgAppWidgetProvider.class), remoteViews);
    }
}           

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

因為桌面小部件的本質是一個廣播元件,是以必須要注冊。

<receiver android:name=".RemoveViews_5.ImgAppWidgetProvider">
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/appwidget_provider_info">
        </meta-data>
    <intent-filter>
        <action android:name="packagename.action.click" />
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
</receiver>           

代碼中有兩個action,第一個是小部件的點選事件,第二個是小部件的辨別必須存在的,如果不加它就不是一個小部件也不會顯示在手機桌面。

小部件的生命周期

onEnable:

當小部件第一次添加到桌面的時候調用,小部件可以添加多次,但是隻在第一次添加的時候調用。

onUpdate:

小部件被添加時或小部件每次更新時都會調用這個方法。每個周期小部件都會自動更新一次,不是點選的時候更新,而是到指定配置檔案時間的時候才更新。

onDelete:

每删除一次小部件就調用一次。

onDisable:

當最後一個該類型的小部件删除時調用該方法。

onRceive:

這是廣播的内置方法,用于分發具體的事件給其他方法,是以該方法一半要調用

super.onReceive(context,intent)

。如果自定義了其他action的廣播,就可以在調用了父類方法之後進行判斷。

PendingIntent介紹

PendingIntent

表示一種處于Pending狀态的意圖,而pending狀态就是表示接下來有一個Intent(即意圖)将在某個待定的時刻發生。它和

Intent

的差別就在于,

Intent

是立刻、馬上發生,而

PendingInten

是将來某個不确定的時刻發生。

PendingIntent的主要方法

PendingIntent

支援三種待定意圖:啟動Activity,啟動Service和發送廣播,分别對應着它的三個接口方法:

getActivity(Context xontext,int requestCode,Intent intent,int flag)

: 獲得一個PendingIntent,該待定意圖發生時,效果相當于

Context.startActivity(intent)

getService(Context xontext,int requestCode,Intent intent,int flag)

Context.startService(intent)

getBroadcast(Context xontext,int requestCode,Intent intent,int flag)

Context.sendBroadcast(intent)

這裡有四個參數,第一個和第三個比較好了解,第二個表示的是PendingIntent發送方的請求碼,大多數情況為0,第四個參數flag常見的類型有四種。

PendingIntent的flag參數

FLAG_ONE_SHOT

: 目前描述的PendingIntent隻能被使用一次,之後被自動cancle,如果後續還有相同的PendingIntent,那麼他們的send方法會調用失敗。

FLAG_NO_CREATE

: 目前描述的PendingIntent不會主動建立,如果目前PendingIntent之前不存在,那麼getActivity,getService,getBroadcast方法會直接傳回null。

FLAG_CANCLE_CURRENT

: 目前的PendingIntent如果已經存在,那麼它們都會被cancle,然後系統會建立一個新的PendingIntent。對于通知欄來說那些被calcle的消息單機後将無法打開。

FLAG_UPDATE_CURRENT

: 目前的PendingIntent如果已經存在,那麼它們都會被更新,即它們的Intent中的Extras會被替換為最新的。

通知欄而言,notify(int, notification)方法中,若id值每次都不同的話,需要考慮到flag參數對應消息接收的情況。

RemoteViews的内部機制

RemoteViews

沒有findViewById方法,是以無法通路裡面的View元素,而必須通過RemoteViews提供的一系列set方法來完成,這是通過反射調用的。

通知欄和小部件分别由

NotificationManager

AppWidgetManger

管理,而

NotificationManager

AppWidgetManger

通過Binder分别和SystemService程序中的

NotificationManagerService

AppWidgetMangerService

中加載的,而它們運作在SystemService中,這就構成了跨程序通信。

構造方法

public RemoteViews(String packageName, int layoutId)

第一個參數是包名,第二個參數是待加載的布局檔案。

支援元件

布局:FrameLayout、LinearLayout、RelativeLayout、GridLayout。

元件:Button、ImageButton、ImageView、ProgressBar、TextView、ListView、GridView、ViewStub等(例如EditText是不允許在RemoveViews中使用的,使用會抛異常)。

工作原理

系統将View操作封裝成Action對象,Action同樣實作了Parcelable接口,通過Binder傳遞到SystemServer程序。遠端程序通過RemoteViews的

apply

方法來進行view的更新操作,RemoteViews的

apply

方法内部則會去周遊所有的action對象并調用它們的

apply

方法來進行view的更新操作。

這樣做的好處是不需要定義大量的Binder接口,其次批量執行RemoteViews中的更新操作提高了程式性能。

工作流程

首先

RemoteViews

會通過Binder傳遞到

SystemService

程序,因為

RemoteViews

實作了Parcelable接口,是以它可以跨程序傳輸,系統會根據

RemoteViews

的包名等資訊拿到該應用的資源;然後通過

LayoutInflater

去加載

RemoteViews

中的布局檔案。接着系統會對View進行一系列界面更新任務,這些任務就是之前我們通過set來送出的。set方法對View的更新并不會立即執行,會記錄下來,等到RemoteViews被加載以後才會執行。這樣

RemoteViews

就可以在SystemService程序中顯示了。

這裡需要注意一個小知識點就是

apply

reApply

方法的差別,

apply

會加載布局并且更新界面,而

reApply

隻會更新界面。

源碼分析

我們下面基于android8.0的源碼看看RemoteViews,set方法之後的邏輯是怎麼樣的,以setTextViewText為例:

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

繼續深入檢視

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

我們發現這裡沒有對View直接操作,而是添加了一個

REflectionAction

對象,進一步檢視:

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

這裡僅僅把action加入了list。下面我們通過

NotificationManager

notify

方法來看看。

public void notify(String tag, int id, Notification notification)
    {
        notifyAsUser(tag, id, notification, new UserHandle(UserHandle.myUserId()));
    }           

進一步檢視

public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
    {
        INotificationManager service = getService();
        String pkg = mContext.getPackageName();
        // Fix the notification as best we can.
        Notification.addFieldsFromContext(mContext, notification);
        if (notification.sound != null) {
            notification.sound = notification.sound.getCanonicalUri();
            if (StrictMode.vmFileUriExposureEnabled()) {
                notification.sound.checkFileUriExposed("Notification.sound");
            }
        }
        fixLegacySmallIcon(notification, pkg);
        if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
            if (notification.getSmallIcon() == null) {
                throw new IllegalArgumentException("Invalid notification (no valid small icon): "
                        + notification);
            }
        }
        if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
        final Notification copy = Builder.maybeCloneStrippedForDelivery(notification);
        try {
            service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
                    copy, user.getIdentifier());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
           

我們注意到這裡最終調用了

INotificationManager

enqueueNotificationWithTag

方法,這裡

INotificationManager

是aidl,通過Binder通信,真正實作它的Java類是

NotificationManagerService

,下面繼續跟進這個方法:

@Override
        public void enqueueNotificationWithTag(String pkg, String opPkg, String tag, int id,
                Notification notification, int userId) throws RemoteException {
            enqueueNotificationInternal(pkg, opPkg, Binder.getCallingUid(),
                    Binder.getCallingPid(), tag, id, notification, userId);
        }           

enqueueNotificationInternal

的源碼,這個方法代碼有點多,我們隻看重要部分。

void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid,
            final int callingPid, final String tag, final int id, final Notification notification,
            int incomingUserId) {
        if (DBG) {
            Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id
                    + " notification=" + notification);
        }
        checkCallerIsSystemOrSameApp(pkg);

        final int userId = ActivityManager.handleIncomingUser(callingPid,
                callingUid, incomingUserId, true, false, "enqueueNotification", pkg);
        final UserHandle user = new UserHandle(userId);

    ......
    省略部分代碼
    ......
        mHandler.post(new EnqueueNotificationRunnable(userId, r));
    }
           

這裡我們看到通過Handler來post了一個Runable對象,暫且先不管這個Runable幹啥的我們往下看它的

run

方法:

protected class EnqueueNotificationRunnable implements Runnable {
        private final NotificationRecord r;
        private final int userId;

        EnqueueNotificationRunnable(int userId, NotificationRecord r) {
            this.userId = userId;
            this.r = r;
        };

        @Override
        public void run() {
            synchronized (mNotificationLock) {
                mEnqueuedNotifications.add(r);
                scheduleTimeoutLocked(r);
                ......
                省略部分代碼
                ......
                } else {
                    mHandler.post(new PostNotificationRunnable(r.getKey()));
                }
            }
        }
    }           

我們看到這裡它又post了一個

PostNotificationRunnable

對象,這又是什麼鬼,我們接着往下看:

protected class PostNotificationRunnable implements Runnable {
        private final String key;

        PostNotificationRunnable(String key) {
            this.key = key;
        }

        @Override
        public void run() {
            synchronized (mNotificationLock) {
                try {
                ......
                省略部分代碼
                ......
                        // ATTENTION: in a future release we will bail out here
                        // so that we do not play sounds, show lights, etc. for invalid
                        // notifications
                        Slog.e(TAG, "WARNING: In a future release this will crash the app: "
                                + n.getPackageName());
                    }

                    buzzBeepBlinkLocked(r);
                } finally {
                    int N = mEnqueuedNotifications.size();
                    for (int i = 0; i < N; i++) {
                        final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
                        if (Objects.equals(key, enqueued.getKey())) {
                            mEnqueuedNotifications.remove(i);
                            break;
                        }
                    }
                }
            }
        }
    }           

我們看到它最終調用了

buzzBeepBlinkLocked

方法,我們進一步檢視它的源碼:

@VisibleForTesting
    @GuardedBy("mNotificationLock")
    void buzzBeepBlinkLocked(NotificationRecord record) {
        ......
        // Should this notification make noise, vibe, or use the LED?
        ......
        // If we're not supposed to beep, vibrate, etc. then don't.
        ......
        // Remember if this notification already owns the notification channels.
        ......
            if (disableEffects == null
                    && canInterrupt
                    && mSystemReady
                    && mAudioManager != null) {
                if (DBG) Slog.v(TAG, "Interrupting!");
                Uri soundUri = record.getSound();
                hasValidSound = soundUri != null && !Uri.EMPTY.equals(soundUri);
                long[] vibration = record.getVibration();
                // Demote sound to vibration if vibration missing & phone in vibration mode.
                ......
        // If a notification is updated to remove the actively playing sound or vibrate,
        // cancel that feedback now
        if (wasBeep && !hasValidSound) {
            clearSoundLocked();
        }
        if (wasBuzz && !hasValidVibrate) {
            clearVibrateLocked();
        }

        // light
        // release the light
        ......
        if (buzz || beep || blink) {
            MetricsLogger.action(record.getLogMaker()
                    .setCategory(MetricsEvent.NOTIFICATION_ALERT)
                    .setType(MetricsEvent.TYPE_OPEN)
                    .setSubtype((buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0)));
            EventLogTags.writeNotificationAlert(key, buzz ? 1 : 0, beep ? 1 : 0, blink ? 1 : 0);
        }
    }           

這個方法很長,但是職責相對來說比較明确,确認是否需要聲音,震動和閃光,如果需要,那麼就發出聲音,震動和閃光。最後将

mBuzzBeepBlinked

post到工作handler,最後會調用到

mStatusBar.buzzBeepBlinked()

,mStatusBar是

StatusBarManagerInternal

對象,這個對象是在

StatusBarManagerService

中初始化,是以最後調用到了StatusBarManagerService中

StatusBarManagerInternal

buzzBeepBlinked()

public void buzzBeepBlinked() {
    if (mBar != null) {
        try {
            mBar.buzzBeepBlinked();
        } catch (RemoteException ex) {
        }
    }
}           

mBar是一個

IStatusBar

對象。關于更進一步的分析看這裡

源碼分析Notification的Notify

。我們最終發現是調用到了

CommandQueue

中,接着sendEmptyMessage給了内部的H類,接着調用了mCallbacks.buzzBeepBlinked()方法,這個mCallbacks就是BaseStatusBar,最終會将notification繪制出來,到這裡一個notification就算是完成了。

RemoteViews的意義

RemoteViews最大的意義應該還是在于它可以跨程序更新UI。

1、當一個應用需要更新另一個應用的某個界面,我們可以選擇用AIDL來實作,但如果更新比較頻繁,效率會有問題,同時AIDL接口就可能變得很複雜。如果采用RemoteViews就沒有這個問題,但RemoteViews僅支援一些常用的View,如果界面的View都是RemoteViews所支援的,那麼就可以考慮采用RemoteViews。

2、利用RemoteViews加載其他App的布局檔案與資源。

感謝

《Android開發藝術探索》

android 特殊使用者通知用法彙總–Notification源碼分析

繼續閱讀