第五章 理解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中显示了出来。修改后的代码如下:



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