Android学习--深入探索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): 获得一个PendingIntent,该待定意图发生时,效果相当于Context.startService(intent)

getBroadcast(Context xontext,int requestCode,Intent intent,int flag): 获得一个PendingIntent,该待定意图发生时,效果相当于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方法来完成,这是通过反射调用的。

通知栏和小部件分别由NotificationManagerAppWidgetManger管理,而NotificationManagerAppWidgetManger通过Binder分别和SystemService进程中的NotificationManagerServiceAppWidgetMangerService中加载的,而它们运行在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进程中显示了。
这里需要注意一个小知识点就是applyreApply方法的区别,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。下面我们通过NotificationManagernotify方法来看看。

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

我们注意到这里最终调用了INotificationManagerenqueueNotificationWithTag方法,这里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中StatusBarManagerInternalbuzzBeepBlinked()方法:

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开发艺术探索》

源码分析Notification的Notify

android 特殊用户通知用法汇总–Notification源码分析

上一篇:初探JAVA代码在虚拟机中的运行机制


下一篇:Android学习——手把手教你实现Android热修复