Android开发艺术探索第五章——理解RemoteViews


这门课的重心在于RemoteViews,RemoteViews可以理解为一种远程的View,其实他和远程的Service是一样的,RemoteViews表示的是一种View的结构,他可以在其他的进程中显示,最常用的就是通知栏和桌面小组件了,我们接下来就细细的品味一下吧!

一.RemoteViews的应用

RemoteViews在实际的开发中,就是通知栏和桌面小组件了,这个大家应该都不陌生,主要是通过NotificationManager的notify方法去实现通知栏,也有通过AppwidthProvider来实现桌面小部件的,其实小部件本质上就是一个广播,他们两个的更新都无法像Activity中直接更新View,这是因为两者都运行在其他大家进程中,确切来说是SystemService中,为了跨进程更新UI,RemoteViews提供了一系列的set方法,我们接下来就是来实际的演示了

1.RemoteViews在通知栏上的应用

首先来看下通知栏,我们先了解一下系统默认的样式

 //系统默认样式
 private void systemStyleNotirfication() {
        Notification notification = new Notification();
        notification.icon = R.mipmap.ic_launcher;
        notification.tickerText = "Hello LiuGuiLin";
        notification.when = System.currentTimeMillis();
        notification.flags = Notification.FLAG_AUTO_CANCEL;
        Intent intent = new Intent(this, TestActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        notification.setLatestEventInfo(this, "LiuGuiLin", "This is Notification", pendingIntent);
        NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        notificationManager.notify(1, notification);
    }

上述的代码会弹出一个系统默认样式的通知栏,单击之后打开TestActivity,看下实际运行效果

虽说如此,但是往往有些需求是不满足的,所以需要自定义了,自定义其实也很简单,我们看下实际代码

    //自定义通知栏
    private void viewStyleNotirfication() {
        Notification notification = new Notification();
        notification.icon = R.mipmap.ic_launcher;
        notification.tickerText = "Hello LiuGuiLin";
        notification.when = System.currentTimeMillis();
        notification.flags = Notification.FLAG_AUTO_CANCEL;
        Intent intent = new Intent(this, TestActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.notification_item);
        remoteViews.setTextViewText(R.id.tv_title, "This is Title");
        remoteViews.setTextViewText(R.id.tv_content, "This is Notification Content");
        remoteViews.setImageViewResource(R.id.iv_img, R.mipmap.ic_launcher);
        remoteViews.setOnClickPendingIntent(R.id.ll_open, pendingIntent);
        notification.contentView = remoteViews;
        NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        notificationManager.notify(2, notification);
    }

我们实际看下运行的结果

可以看到,这是我自己写的布局notification_item

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_open"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_vertical"
    android:orientation="horizontal">

    <ImageView
        android:id="@+id/iv_img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="center_vertical"
        android:orientation="vertical"
        android:paddingLeft="10dp">

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@android:color/black" />

        <TextView
            android:id="@+id/tv_content"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@android:color/black" />

    </LinearLayout>

</LinearLayout>

这就是非常简单的通知栏了

RemoteViews的使用也是非常的简单,只要提供当前应用的包名和布局文件的资源ID就可以创建一个RemoteViews了,如何更新呢?这一点和View还是有很大的不同,RemoteViews更新的时候,无法直接访问里面的View,必须通过RemoteViews所提供的一系列方法来更新,比如设置TextView,那就需要

 remoteViews.setTextViewText(R.id.tv_title, "This is Title");

这里一共两个参数,一个是id,一个就是内容了,图片也是类似

remoteViews.setImageViewResource(R.id.iv_img, R.mipmap.ic_launcher);

如果需要点击事件的话,需要setOnClickPendingIntent来触发了,关于PendingIntent,他表示点击就是一种待定的意图,触发后才会操作,我们后面具体介绍

2.RemoteViews 在桌面小部件的应用

AppWidgetProvider是Android提供给我们的用于实现桌面小部件的类,其本质也就是一个广播,,所以实际使用中把他看成一个广播即可,我们来看下怎么去具体的实现一个小部件

1.定义小部件的界面

在res/layout下我们先写个widget.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/iv1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher" />

    <ImageView
        android:id="@+id/iv2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher" />

    <TextView
        android:id="@+id/tv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Test"
        android:textSize="20dp" />

</LinearLayout>

这里我写了两个图片和一个文本

2.定义小部件配置信息

在res/xml中定义一个appwidget_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="86400000">

</appwidget-provider>

上面的几个参数的含义很明确,android:initialLayout就是加载布局其他两个就是最小的高宽,而updatePeriodMillis就是更新小组件的时间周期

3.定义小组件的实现类

这个类需要继承AppWidgetProvider

package com.liuguilin.remotrview.provider;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.os.SystemClock;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.Toast;

import com.liuguilin.remotrview.R;

/*
 *  项目名:  RemoteViewsSample
 *  包名:    com.liuguilin.remotrview.provider
 *  文件名:   AppWidgetImpl
 *  创建者:   LiuGuiLin
 *  创建时间:  2017/1/14 0014 下午 4:35
 *  描述:    小组件
 */
public class AppWidgetImpl extends AppWidgetProvider {

    public static final String TAG = "AppWidgetImpl";
    public static final String CLICK_ACTION = "com.liuguilin.remotrview.provider.click_action";

    private AppWidgetManager appWidgetManage;
    private float degree;
    private Bitmap bitmap;

    public AppWidgetImpl() {
        super();
    }

    @Override
    public void onReceive(final Context context, final Intent intent) {
        super.onReceive(context, intent);
        String action = intent.getAction();
        Log.i(TAG, "action:" + action);
        if (CLICK_ACTION.equals(action)) {
            Toast.makeText(context, "click it", Toast.LENGTH_SHORT).show();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher);
                    appWidgetManage = AppWidgetManager.getInstance(context);
                    for (int i = 0; i < 37; i++) {
                        degree = (i * 10) % 360;
                        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
                        remoteViews.setImageViewBitmap(R.id.iv1, rotateBitmap(bitmap));
                        remoteViews.setImageViewBitmap(R.id.iv2, rotateBitmap(bitmap));
                        Intent intentClick = new Intent(CLICK_ACTION);
                        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
                        remoteViews.setOnClickPendingIntent(R.id.iv1, pendingIntent);
                        appWidgetManage.updateAppWidget(new ComponentName(context, AppWidgetImpl.class), remoteViews);
                        SystemClock.sleep(30);
                    }
                }
            }).start();
        }
    }

    //每次更新都会调用
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        Log.i(TAG, "onUpdate");
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
        Intent intentClick = new Intent(CLICK_ACTION);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
        remoteViews.setOnClickPendingIntent(R.id.iv1, pendingIntent);
        appWidgetManage.updateAppWidget(new ComponentName(context, AppWidgetImpl.class), remoteViews);
    }

    //动画
    private Bitmap rotateBitmap(Bitmap bitmap) {
        Matrix matrix = new Matrix();
        matrix.reset();
        matrix.setRotate(degree);
        Bitmap temBitmap = Bitmap.createBitmap(this.bitmap, 0, 0, this.bitmap.getWidth(), this.bitmap.getHeight(), matrix, true);
        return temBitmap;
    }
}

上述的代码实现了一个简单的桌面小部件,这里加了个动画,其他到没什么。我们需要注册才能使用

4.在清单文件中声明小部件

<receiver android:name=".provider.AppWidgetImpl">
            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/appwidget_info" />

            <intent-filter>
                <action android:name="com.liuguilin.remotrview.provider.click_action" />
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>
 </receiver>

上面的代码有两个Action,其中第一个是识别小部件的动作,第二个就是他的标识,必须存在,这是系统的规范

AppWidgetProvider 除了最常用的onUpdate方法,还有其他的方法,onEnable,onDisabled,onDeleted以及onReceiver,这些方法都会被onReceiver在适当的时候调用,所以含义如下

  • onEnable:当该窗口小部件第一次添加到桌面的时候调用该方法,可添加多次但是只有第一次调用
  • onUpdate:小部件被添加或者第一次更新的时候都会调用一次该方法,小部件的更新机制由updatePeriodMillis来指定
  • onDeleted:没删除一次小部件都会调用
  • onDisabled:当最后一个该类型的小部件会删除时调用
  • onReceiver:广播内置的方法

关于AppWidgetProvider 的onReceiver方法的具体分发过程,可以看源码中的实现,如下图,通过下面的代码可以看出,onReceiver会根据不同的Action来分别调用这几个方法

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

上面描述了一个开发桌面小部件的完整过程 ,例子比较简单,实际开发过程中会稍微的复杂一些,但是开发流程都是一样的, 可以发现,桌面小程序在界面上的操作都是通过RemoteViews,不管小部件的界面初始化还是界面的更新都需要依赖他

3.PendingIntent概述

我们在前面多次提到了PendingIntent,归根结底,他和Intent有什么区别呢我们就来说下

顾名思义,PendingIntent表示处于一种等待的意图,即特定,等待的意思,就是说你要在某种条件下才会触发,所以我们很容易的就联想到RemoteViews了

PendingIntent有三种待定的意图,就是Activity,Service,广播,可以看如下图

如图中所示,这三个方法的参数都是一样的,主要理解的是第二个参数requstCode和第四个参数flags,code代表的是发送码,多数情况下为0,而且code会影响到flag,flag常见的有几种我们下面会说,其实最主要是理解匹配规则,

PendingIntent的匹配规则为:如果两个PendingIntent他们内部的Intent相同并且requstCode也相同的话,那么PendingIntent就是相同的,code比较好理解,那什么情况下Intent相同呢,Intent的匹配规则是:如果两个Intent的ComponentName的匹配过程,只要Intent之间的ComponentName和intent-filter相同,那么这两个intent就相同,需要注意的是Extras不参与匹配过程,只要intent之间的name和intent-filter相同就行,我们再来说下flags的参数含义

  • FLAG_ONE_SHOT

    当前描述的PendingIntent只能被使用一次,然后他就会被cancel,如果后续还有相同的PendingIntent,那么他的send方法就会失败,对于通知栏的消息来说,如果采用此标记位,那么同类的通知只能使用一次,后续将无法打开

  • FLAG_NO_CREATE

    当前描述的PendingIntent不会主动去创建,如果当前PendingIntent之前不存在,那么getActivity等方法都会直接返回null,即获取PendingIntent失败,这个标记位很少见,他无法单独使用,因此在日常开发当中,并没有太多的意义,这里就不过多的介绍了

  • FLAG_CANCEL_CURRENT

    当前描述的PendingIntent如果已经存在,那么就会被cancel,然后系统创建一个新的PendingIntent,对于通知栏来说,那些被cancel的消息将无法被打开

  • FLAG_UPDATE_CURRENT

    当前描述的PendingIntent如果已经存在的话,那么他们就会被更新,他们的intent中的extras会被替换成新的

从上面的分析看还是不太好理解这四个标记位,下面结合实际的项目来,这里分析两种情况,如下代码,如果notify的第一个参数id是常量,那么多次通知就只能弹出一个通知,后续会把前面的替换掉,如果每次不一样,就会多弹出几个通知

如果notify方法的id是常量,那么不管PendingIntent是否匹配,后面的通知会替换前面的通知,这个很好理解

如果notify方法的id每次不同,那么当PendingIntent不匹配时,这里的匹配是指PendingIntent中Intent相同切requstCode相同,在这种情况下不管采用了何种标记位,这些通知之间不互相干扰。如果PendingIntent处于匹配状态,这个时候要分情况讨论,如果采用FLAG_ONE_SHOT标记位,那么后续通知中,PendingIntent会和第一条通知保持一致,包括其中的Extras,单击任何一条通知,剩余的都无法打开当所有的通知被清除后,会重复这个过程,如果采用FLAG_CANCEL_CURRENT标记位,那么只有最新的通知可以打开,之前弹出的所有通知均无法打开,如果采用FLAG_UPDATE_CURRENT标记位。那么之前弹出的通知PendingIntent会被更新,最终他们和最新的一条通知保持一致,包括其中的Extras,那么这些通知都可以被打开

二.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,那么通知栏消息无法将弹出并且会抛出异常

RemoteViews也没有提供findViewById方法,因此无法直接访问里面的View元素,而必须通过RemoteViews所提供的一系列set方法来完成,当然这是因为RemoteViews在远程进程中显示,所以没办法直接findViewById,关于set方法,可以看下这表

从这张表可以看出,原本可以直接调用View的方法,现在都需要通过set来完成,而从这些方法的声明来看,很像是通过反射来完成的,事实上也是如此

下面我们继续来说下RemoteViews的内部机制,由于RemoteViews主要用于通知栏和通知栏和桌面小部件,这里说一下他的工作过程,我们知道小部件分别由NotificationManager和AppWidgetProvider管理,而NotificationManager和AppWidgetProvider通过Binder分别为SystemService进程中的NotificationManagerService和AppWidgetService,由此可见,通知栏和小部件实际上是这两个中加载出来的,这就是我们的进程构成了跨进程通信的原理

首先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接口。系统首先将Vew操作封装装到Action对象并将这些对象跨进程传输到远程进程,接着在远程进程中执行Action对象中对象,当我们通过NotificationManager和AppWidgetManager来提交我们的更新时,这些Action对象就会传输到远程进程并在远程进程中依次执行,这个过程可以参看下面的图片,远程进程通过RemoteViews的apply方法来进行View的更新操作,RemoteViews的apply方法内部则回去遍历所有的Action对象并调用它们的apply方法,具体的View更新操作是由Acton对apply方法来完成的。上述做法的好处是显而易见的,首先不需要定义大量的Binder,其次通过在远程进程中批量执行RemoteViews的修改操作从而避免了大量的IPC视作这就提高了程序的性能,由此可见,Android系统在这方面的设计的确很精妙。

上面从理论上分析了RemoteViews的内部机制,接下来我们从源码的角度分析下Remoteviews的工作流程。它的构造方法就不用多说了,这里我们首先看他一系列的set方法,比如setTextViewText

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

上述ID中,viewId是被操作的View的id,setText是一个方法名,text是给TextView要设置的文本,可以联想一下,是不是清晰了很多,我们再来看下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);
    }

从上述代码可以得知RemoteViews内部有一个mActions 成员,他是一个ArrayList,外界每一次调用一个set方法,他都能保存下来并未对view进行实际的操作,这一点从上面的理论分析就可以知道,我们继续看,我们现在看一下apply的实现以及Action类的实现

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

performApply的实现是比较好理解的,他的作用就是遍历mActions 这个列表并执行每一个Action对象的apply方法,还记得mAction吗?每一次的set操作都会对应他里面的一个Action对象,因此也可以断定,Action对象的apply方法就是真正操作View的地方,实际上也是如此

RemoteViews在通知栏和桌面小部件中的工作过程和上面描述的过程是一致的,当我们调用RemoteViews的set方法时,并不会立刻更新它们的界面,而必须要通过NotificationManager的notify方法以及AppWidgetManager的updateAppWidget才能更新它们的界面。实际上在AppWidgetManager的 updateAppWidget的内部实现中,它们的确是通过RemoteViews的apply以及reapply方法来加载或者更新界面的,apply和reApply的区别在于:apply会加载布局并更新界面,而reApply则只会更新界面。通知栏和桌面小插件在初始化界面时会调用apply方法,而在后续的更新界面时则会调用reapply方法。这里先看一

下 BaseStatusBar的updateNotificationViews方法中,如下所示。

    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);
        if (bigContentView != null && entry.getBigContentView() != null) {
            bigContentView.reapply(mContext, entry.getBigContentView(),
                    mOnClickHandler);
        }
        if (publicContentView != null && entry.getPublicContentView() != null) {
            publicContentView.reapply(mContext, entry.getPublicContentView(), mOnClickHandler);
        }
        // update the contentIntent
        final PendingIntent contentIntent = notification.getNotification().contentIntent;
        if (contentIntent != null) {
            final View.OnClickListener listener = makeClicker(contentIntent, notification.getKey(),
                    isHeadsUp);
            entry.row.setOnClickListener(listener);
        } else {
            entry.row.setOnClickListener(null);
        }
        entry.row.setStatusBarNotification(notification);
        entry.row.notifyContentUpdated();
        entry.row.resetHeight();
    }

很显然,上述代码表示当通知栏需要更新的时候,会通过RemoteViews的reapply方法来更新

接下来我们看一下AppWidgetHostView的updateAppWidget方法

    public void updateAppWidget(int[] appWidgetIds, RemoteViews views) {
        if (mService == null) {
            return;
        }
        try {
            mService.updateAppWidgetIds(mPackageName, appWidgetIds, views);
        }
        catch (RemoteException e) {
            throw new RuntimeException("system server dead?", e);
        }
    }

从上面的代码,我们是通过IAppWidgetService的接口去实现的,实际上最红也是通过RemoteViews的reapply方法来更新

了解了apply和reapply的作用之后我们继续看Action的子类实现,首先看下ReflectionAction的具体实现,他的源码如下:

    private final class ReflectionAction extends Action {
        static final int TAG = 2;

        static final int BOOLEAN = 1;
        static final int BYTE = 2;
        static final int SHORT = 3;
        static final int INT = 4;
        static final int LONG = 5;
        static final int FLOAT = 6;
        static final int DOUBLE = 7;
        static final int CHAR = 8;
        static final int STRING = 9;
        static final int CHAR_SEQUENCE = 10;
        static final int URI = 11;
        // BITMAP actions are never stored in the list of actions. They are only used locally
        // to implement BitmapReflectionAction, which eliminates duplicates using BitmapCache.
        static final int BITMAP = 12;
        static final int BUNDLE = 13;
        static final int INTENT = 14;
        static final int COLOR_STATE_LIST = 15;

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

        ReflectionAction(Parcel in) {
            this.viewId = in.readInt();
            this.methodName = in.readString();
            this.type = in.readInt();
            //noinspection ConstantIfStatement
            if (false) {
                Log.d(LOG_TAG, "read viewId=0x" + Integer.toHexString(this.viewId)
                        + " methodName=" + this.methodName + " type=" + this.type);
            }

            // For some values that may have been null, we first check a flag to see if they were
            // written to the parcel.
            switch (this.type) {
                case BOOLEAN:
                    this.value = in.readInt() != 0;
                    break;
                case BYTE:
                    this.value = in.readByte();
                    break;
                case SHORT:
                    this.value = (short)in.readInt();
                    break;
                case INT:
                    this.value = in.readInt();
                    break;
                case LONG:
                    this.value = in.readLong();
                    break;
                case FLOAT:
                    this.value = in.readFloat();
                    break;
                case DOUBLE:
                    this.value = in.readDouble();
                    break;
                case CHAR:
                    this.value = (char)in.readInt();
                    break;
                case STRING:
                    this.value = in.readString();
                    break;
                case CHAR_SEQUENCE:
                    this.value = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
                    break;
                case URI:
                    if (in.readInt() != 0) {
                        this.value = Uri.CREATOR.createFromParcel(in);
                    }
                    break;
                case BITMAP:
                    if (in.readInt() != 0) {
                        this.value = Bitmap.CREATOR.createFromParcel(in);
                    }
                    break;
                case BUNDLE:
                    this.value = in.readBundle();
                    break;
                case INTENT:
                    if (in.readInt() != 0) {
                        this.value = Intent.CREATOR.createFromParcel(in);
                    }
                    break;
                case COLOR_STATE_LIST:
                    if (in.readInt() != 0) {
                        this.value = ColorStateList.CREATOR.createFromParcel(in);
                    }
                    break;
                default:
                    break;
            }
        }

        public void writeToParcel(Parcel out, int flags) {
            out.writeInt(TAG);
            out.writeInt(this.viewId);
            out.writeString(this.methodName);
            out.writeInt(this.type);
            //noinspection ConstantIfStatement
            if (false) {
                Log.d(LOG_TAG, "write viewId=0x" + Integer.toHexString(this.viewId)
                        + " methodName=" + this.methodName + " type=" + this.type);
            }

            // For some values which are null, we record an integer flag to indicate whether
            // we have written a valid value to the parcel.
            switch (this.type) {
                case BOOLEAN:
                    out.writeInt((Boolean) this.value ? 1 : 0);
                    break;
                case BYTE:
                    out.writeByte((Byte) this.value);
                    break;
                case SHORT:
                    out.writeInt((Short) this.value);
                    break;
                case INT:
                    out.writeInt((Integer) this.value);
                    break;
                case LONG:
                    out.writeLong((Long) this.value);
                    break;
                case FLOAT:
                    out.writeFloat((Float) this.value);
                    break;
                case DOUBLE:
                    out.writeDouble((Double) this.value);
                    break;
                case CHAR:
                    out.writeInt((int)((Character)this.value).charValue());
                    break;
                case STRING:
                    out.writeString((String)this.value);
                    break;
                case CHAR_SEQUENCE:
                    TextUtils.writeToParcel((CharSequence)this.value, out, flags);
                    break;
                case URI:
                    out.writeInt(this.value != null ? 1 : 0);
                    if (this.value != null) {
                        ((Uri)this.value).writeToParcel(out, flags);
                    }
                    break;
                case BITMAP:
                    out.writeInt(this.value != null ? 1 : 0);
                    if (this.value != null) {
                        ((Bitmap)this.value).writeToParcel(out, flags);
                    }
                    break;
                case BUNDLE:
                    out.writeBundle((Bundle) this.value);
                    break;
                case INTENT:
                    out.writeInt(this.value != null ? 1 : 0);
                    if (this.value != null) {
                        ((Intent)this.value).writeToParcel(out, flags);
                    }
                    break;
                case COLOR_STATE_LIST:
                    out.writeInt(this.value != null ? 1 : 0);
                    if (this.value != null) {
                        ((ColorStateList)this.value).writeToParcel(out, flags);
                    }
                default:
                    break;
            }
        }

        private Class<?> getParameterType() {
            switch (this.type) {
                case BOOLEAN:
                    return boolean.class;
                case BYTE:
                    return byte.class;
                case SHORT:
                    return short.class;
                case INT:
                    return int.class;
                case LONG:
                    return long.class;
                case FLOAT:
                    return float.class;
                case DOUBLE:
                    return double.class;
                case CHAR:
                    return char.class;
                case STRING:
                    return String.class;
                case CHAR_SEQUENCE:
                    return CharSequence.class;
                case URI:
                    return Uri.class;
                case BITMAP:
                    return Bitmap.class;
                case BUNDLE:
                    return Bundle.class;
                case INTENT:
                    return Intent.class;
                case COLOR_STATE_LIST:
                    return ColorStateList.class;
                default:
                    return null;
            }
        }

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

        public int mergeBehavior() {
            // smoothScrollBy is cumulative, everything else overwites.
            if (methodName.equals("smoothScrollBy")) {
                return MERGE_APPEND;
            } else {
                return MERGE_REPLACE;
            }
        }

        public String getActionName() {
            // Each type of reflection action corresponds to a setter, so each should be seen as
            // unique from the standpoint of merging.
            return "ReflectionAction" + this.methodName + this.type;
        }
    }

通过上述的代码可以看出,ReflectionAction 表示的是一个反射的动作,他通过对View的操作会反射的方式调用,其中getMethod就是根据方法名来反射所需要的方法,其中ReflectionAction 也有很多的set方法,除了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(TAG);
            dest.writeInt(viewId);
            dest.writeInt(units);
            dest.writeFloat(size);
        }

        @Override
        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
            final TextView target = (TextView) 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;
    }

他的实现是比较简单的,他之所以不用反射来实现,是因为setTextView这个方法有灵感参数,因此无法复用ReflectionAction,因为ReflectionAction 的反射调用只需要一个参数,这里就不一一分析了

关于单击事件,RemoteView中只支持PendingIntent,是不支持其他模式的,我们这里需要注意的就是setOnClickPendingIntent,setPendingIntentTemplate,以及setonClickFillinIntent的区别,首先setOnClickPendingIntent用于给普通view设置单击事件,但是不能给ListView之类的View设置,如果需要,就用后两者

三.RemoteViews的意义

这一章,我们来真正意义上的理解他的意义,通过打造一个模拟的通知栏效果并且实现跨进程的UI更新

首先我们两个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中,代码如下所示。

public class BActivity extends Activity implements View.OnClickListener {

    private Button btn_send;

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

    }

    private void initView() {
        btn_send = (Button) findViewById(R.id.btn_send);
        btn_send.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_send:
                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("send_bro");
                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("send_bro");
            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("send_bro");
        registerReceiver(mBroadcastReceiver, intent);
    }

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

上述代码很简单,除了注册和解除广播以外,最主要的逻辑其实就是updateUI,当A收到广播后,会从Intent中取出RemoteViews对象,然后通过apply方法加载布局并且执行更新操作,最后将得到的View添加到A的布局中即可。可以发现这个过程很简单,但是通知栏的底层是如何实现的呢?

木节这个例子是可以在实际中使用的,比如现在有两个应用,一个应用需要能够事更新另一个应用中的某个界面,这个时候我们当然可以选择AIDL去实现,但是如果对界面的更新比较频繁,这个时候就会有效率问题,同时AIDL接口就有可能会变得很复杂。这个时间如果采用RemoteViews来实现就没有这个问题了,当然RemoteViews也有缺点,那就是他只支持一些常见的View,对于自定义View它是不支持的。面对这种问题,到底是采用AIDL还是采用RemoteViews,这个要看具体情况,如果界面中的View都是一些简单的且被RemoteViews支持的View,那么可以考虑采用RemoteViews,否则就不适合用RemoteViews 了。

如果打算采用RemoteViews来实现两个应用之间的界面更新,那么这里还有一个问题,那就是布局文件的加载问题。在上面的代码中,我们直接通过RemoteViews的的apply方法来加载并更新界面,如下所示。’

 View view = remoteViews.apply(this, mLinearLayout);
 mLinearLayout.addView(view);

这种写法在同一个应用的多进程情形下是适用的,但是如果A和B属于不同应用,那么B中的布局文件的资源id传输到A中以后很有可能是无效的,因为A中的这个布局文件的资源id不可能刚好和B中的资源id一样,面对这种情况,我们就要适当的修改Remoteviews的显示过程的代码了。这里给出一种方法,既然资源不相同,那我们就通过资源名称来加载布局文件。首先两个应用要提前约定好RemoteViews中的布局的文件名称,比如“layout simulated notification”,然后在A中根据名称找到并加载,接着再调用Remoteviews 的的reapply方法即可将B中对View所做的一系列更新操作加载到View上了,关于applyHe reapply方法的差别在前面说了,这样历程就OK了

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

到这里,这章也就结束了,理解的很深收益很大,这本书真的不错哦

有兴趣就加群吧:555974449

PPT下载:点击下载

Sample下载:点击下载

Android开发艺术探索第五章——理解RemoteViews的更多相关文章

  1. Android开发艺术探索笔记——第一章:Activity的生命周期和启动模式

    Android开发艺术探索笔记--第一章:Activity的生命周期和启动模式 怀着无比崇敬的心情翻开了这本书,路漫漫其修远兮,程序人生,为自己加油! 一.序 作为这本书的第一章,主席还是把Activ ...

  2. [Android]Android开发艺术探索第13章笔记

    13.1 使用CrashHandler来获取应用的Crash信息 (1)应用发生Crash在所难免,但是如何采集crash信息以供后续开发处理这类问题呢? 利用Thread类的setDefaultUn ...

  3. [Android]Android开发艺术探索第1章笔记

    1.1 Activity 的生命周期全面分析 1.1.1 典型情况下的生命周期分析 onPause: 正在停止,正常情况下紧接着 onStop 就会被调用,然后新的 Activity 执行 onRes ...

  4. Android开发艺术探索——第二章:IPC机制(中)

    Android开发艺术探索--第二章:IPC机制(中) 好的,我们继续来了解IPC机制,在上篇我们可能就是把理论的知识写完了,然后现在基本上是可以实战了. 一.Android中的IPC方式 本节我们开 ...

  5. Android开发艺术探索——第二章:IPC机制(上)

    Android开发艺术探索--第二章:IPC机制(上) 本章主要讲解Android的IPC机制,首先介绍Android中的多进程概念以及多进程开发模式中常见的注意事项,接着介绍Android中的序列化 ...

  6. 《Android开发艺术探索》读书笔记 (13) 第13章 综合技术、第14章 JNI和NDK编程、第15章 Android性能优化

    第13章 综合技术 13.1 使用CrashHandler来获取应用的Crash信息 (1)应用发生Crash在所难免,但是如何采集crash信息以供后续开发处理这类问题呢?利用Thread类的set ...

  7. 《Android开发艺术探索》读书笔记 (9) 第9章 四大组件的工作过程

    第9章 四大组件的工作过程 9.1 四大组件的运行状态 (1)四大组件中只有BroadcastReceiver既可以在AndroidManifest文件中注册,也可以在代码中注册,其他三个组件都必须在 ...

  8. 《android开发艺术探索》读书笔记(十五)--Android性能优化

    接上篇<android开发艺术探索>读书笔记(十四)--JNI和NDK编程 No1: 如果<include>制定了这个id属性,同时被包含的布局文件的根元素也制定了id属性,那 ...

  9. 《android开发艺术探索》读书笔记(五)--RemoteViews

    接上篇<android开发艺术探索>读书笔记(四)--View工作原理 No1: RemoteViews使用场景:通知栏和桌面小部件 No2: 通知栏主要通过NotificationMan ...

随机推荐

  1. [转]map函数补充

    map()函数 map()是 Python 内置的高阶函数,它接收一个函数 f 和一个 list,并通过把函数 f 依次作用在 list 的每个元素上,得到一个新的 list 并返回. 例如,对于li ...

  2. transition和animation做动画(css动画二)

    前言:这是笔者学习之后自己的理解与整理.如果有错误或者疑问的地方,请大家指正,我会持续更新! translate:平移:是transform的一个属性: transform:变形:是一个静态属性,可以 ...

  3. Spring AOP异常捕获原理

    Spring AOP异常捕获原理:        被拦截的方法,须显式的抛出异常,且不能做任何处理, 这样AOP才能捕获到方法中的异常,进而进行回滚.        换句话说,就是在Service层的 ...

  4. 修改 iOS AppIcon

    有一次看到亚马逊的客户端打开后,就提示 icon 改变了,回桌面一看,竟然真的变了.然后就上网搜索是怎么实现的,参照着写了一个小 demo ,权当笔记. 首先,导入图片到项目的中,如下图.不要导入到 ...

  5. LeetCode169:Majority Element(Hash表\位操作未懂)

    题目来源: Given an array of size n, find the majority element. The majority element is the element that ...

  6. Words to Use Instead of "Very"

  7. codeforces868D Huge Strings

    You are given n strings s1, s2, ..., sn consisting of characters 0 and 1. m operations are performed ...

  8. [HAOI2008]硬币购物

    题目描述 硬币购物一共有4种硬币.面值分别为c1,c2,c3,c4.某人去商店买东西,去了tot次.每次带di枚ci硬币,买si的价值的东西.请问每次有多少种付款方法. 输入输出格式 输入格式: 第一 ...

  9. UVALive - 3938:"Ray, Pass me the dishes!"

    优美的线段树 #include<cstdio> #include<cstdlib> #include<algorithm> #include<cstring& ...

  10. hihocoder1257(构造)(2015北京ACM/ICPC)

    题意: 给你n条蛇,a[i]的长度为i,要求组成一个矩形.奇数蛇可折叠奇数次,偶数蛇折叠偶数次,然后按蛇的次序输出 (即一条蛇的输出只能是一个方向的) 2 3 1 2 1 3 2 3 1 1 2 1 ...