RemoteView 使用详解与实现原理

-

RemoteView 详解

  • RemoteView,是一种用于跨进程显示View的工具,使用场景一般为显示通知和实现桌面控件
  • 相对于Binder,RemoteView效率更高但限制也更多
  • 下面以桌面控件 AppWidget 为例,讲解RemoteView的使用

AppWidget

  • AppWidget 虽然是一种可选的界面,但是它能够十分方便地为用户提供信息,更重要的是它为整个应用提供了快捷的入口,网上关于AppWidget的文章虽然多但是写得都不是很清晰,忽略了一些需要注意的坑,希望这篇文章能够提供更全面的讲解,写AppWidget主要分为以下几个步骤
1.使用XML文件为控件提供元数据(AppWidgetProviderInfo)
2.使用XML文件为控件提供布局信息
3.实现AppWidgetProvider管理控件的生命周期和提供数据
4.使用Service更新界面(可选)
5.实现RemoteViewsService和RemoteViewsFactory为控件提供更复杂的布局及其点击事件的响应(可选)
6.在manifest文件中注册AppWidgetProvider和用到的service 
  • 我们首先来观察manifest文件,下面是一个简单的例子,从这个例子我们可以知道 AppWidgetProvider 虽然名字比较有误导性,但它其实是一个BroadcastReceiver,标签中的meta-data就是刚刚说的AppWidgetProviderInfo,intent-filter中必须要有android.appwidget.action.APPWIDGET_UPDATE 这个ACTION,如果有其他需求还可以添加自定义的ACTION, 就是我定义的广播,用于更新这个AppWidget
<receiver android:name=".widget.WidgetProvider">
    <intent-filter>

        <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
        <action android:name="com.linjiamin.UPDATE"/>
    </intent-filter>
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/app_widget"/>
</receiver> 

AppWidgetProviderInfo

  • meta-data中的resource属性指向的布局文件应该写在res的xml包下,如果没有这个包则新建即可,这个xml对应于AppWidgetProviderInfo类的类属性,包含了整个AppWidget的配置信息
<!--in app_widget.xml-->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/app_widget"
    android:minHeight="120dp"
    android:minWidth="120dp"
    android:previewImage="@mipmap/ic_launcher"
    android:resizeMode="horizontal|vertical"
    android:updatePeriodMillis="3000"
    android:widgetCategory="home_screen|keyguard">
</appwidget-provider>
  • AppWidgetProviderInfo 有如下几个属性可供配置
minWidth 和 minHeight:AppWidget最小的布局宽高,但不是确切的布局宽高,AppWidget的尺寸的最小单位为桌面每个网格的尺寸,如果不足整数个网格将向上取整,手机上不建议大于4*4个网格

minResizeWidth 和 minResizeHeight ,允许用户重新调整尺寸,minResizeWidth可以小于minWidth,但实际尺寸不能小于minResizeWidth,minResizeHeight同理

updatePeriodMillis :widget的更新频率,即是更新的回调方法onUpdate粗略的调用频率,在1.6以后,频率不能高于30分钟一次,否则会被设定为30分钟一次,由于更新会唤醒设备所以会有一定的能耗,如果使用 alarm来替代该属性,alarm应设置为 ELAPSED_REALTIME 或 RTC,同时将 updatePeriodMillis 设为 0。

initialLayout : widget 的布局资源文件,只能包含特定的控件

configure:当 widget 创建时,会自动启动填写的Activity,这个Activity应用于进行配置信息

previewImage:预览图,如果没有提供则使用应用图标。

utoAdvanceViewId :指定一个子view ID,表明该子 view 会自动更新

resizeMode : horizontal, vertical, none,分别为可拉伸的方向

widgetCategory:home_screen,keyguard,指定widget是否在桌面和锁屏界面上显示

initialKeyguardLayout : 锁屏界面的布局资源文件

主布局

  • 布局,RemoteView只能支持一下几种布局,这个例子中只用到了TextView和ListView,和普通控件的布局没有差别
//可供使用的布局
Layout: FrameLayout,LinearLayout,RelativeLayout,GridLayout

View:AnalogClock,Button,Chronometer,ImageButton,ImageView,ProgressBar ,TextView,ViewFlipper,ListView,GridView,StackView,AdapterViewFilter,ViewStub

<?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:gravity="center_horizontal"
android:orientation="vertical">

<TextView
android:id="@+id/widget_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="36sp"
android:textStyle="bold"/>

<ListView
android:id="@+id/widget_lv"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</ListView>
</LinearLayout>

AppWidgetProvider

  • AppWidgetProvider,AppWidgetProvider的onReceive中会接收诸如 APPWIDGET_UPDATE,APPWIDGET_DELETED等ACTION,然后调用相应的生命周期方法,如果我们要接收自定义的广播,需要重写onReceive然后调用自己的方法
  • onReceive使用的几个回调方法为,onUpdate() ,RemoteView在需要更新数据时调用,onDeleted(),AppWidget被移除时调用 ,onEnabled,AppWidget被实例化时调用,重复实例化时不会反复调用该方法,onDisabled(),AppWidget的最后一个实例被delete时调用
  • 由于XML中配置的更新频率不能大于30分钟一次,所以这里实现一个Service来更新,在onReceive中接受到自定义的ACTION,然后调用自定义的update方法,在onEnabled中启动发送广播的service,
public class WidgetProvider extends AppWidgetProvider {

public static int mIndex = 0;
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
if(intent.getAction().equals(UpdateService.ACTION_UPDATE)){
    Update(context);
    Log.d(TAG, "onReceive: ");
    }
}

@Override
public void onEnabled(Context context) {

Intent intent = new Intent(context, UpdateService.class);
context.startService(intent);
Log.d(TAG, "onEnabled: ");
super.onEnabled(context);
}

@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle
    newOptions) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
}

@Override
public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) {
super.onRestored(context, oldWidgetIds, newWidgetIds);
}

@Override
public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
}

@Override
public void onDisabled(Context context) {
Intent intent = new Intent(context,UpdateService.class);
context.stopService(intent);
super.onDisabled(context);
}
  • 更新RemoteView需要获取AppWidgetManager的实例并调用updateAppWidget方法传入包名或id和新的remoteViews
private void Update(Context context) {
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.app_widget);
    remoteViews.setTextViewText(R.id.widget_tv, ""+mIndex);
    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(CurriculumApplication.getContext());
    appWidgetManager.updateAppWidget(new ComponentName(CurriculumApplication.getContext(), WidgetProvider.class), remoteViews);
mIndex ++;
    }
}
  • 发送广播的service这里用计时器来实现,这里每隔一秒发送一次广播,当然最好是使用AlarmManager的ELAPSED_REALTIME 或 RTC来实现,避免不必要地唤醒设备
public class UpdateService extends Service{

private Timer mTimer;
private TimerTask mTimerTask;
public static final int UPDATE_TIME = 1000;
public static final String ACTION_UPDATE = "com.linjiamin.UPDATE";

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}

@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public void onCreate() {
super.onCreate();

Log.d(TAG, "onCreate: ");
mTimer = new Timer();
mTimerTask = new TimerTask() {
    @Override
    public void run() {
        Intent updateIntent = new Intent(ACTION_UPDATE);
        sendBroadcast(updateIntent);
    }
};
mTimer.schedule(mTimerTask, 1000, UPDATE_TIME);
    }

        @Override
    public void onDestroy() {
    super.onDestroy();
    mTimer.cancel();
    }
}
  • 现在来看回我们的AppWidgetProvider,还有一个很重要的方法没有讲解——onUpdate(),onUpdate()是系统每次决定更新时调用的方法,我们刚刚通过service实现的界面更新是不会调用到这个方法的,如果更新的频率不高于30分钟一次,那么在这个方法中实现更新的逻辑即可,注意对于ListView,StackView这些包含Item的复杂控件需要使用RemoteViewService获取Adapter,这些稍后再讲解
  • 通常onUpdate()还会做两件事,设置点击事件和PendingIntent的模板,在这里我们为TextView设定了一个启动Activity的PendingIntent,跟Notification类似,没有什么好讲的,接着为ListView设置了适配器和PendingIntent的模板,为ListView的每一个Item设置PendingIntent是很浪费的,为了提高效率,必须在AppWidgetProvider中调用setPendingIntentTemplate,传入的PendingIntent作为模板,之后RemoteViewsFactory可以利用这个模板和新的Intent进行合成
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {

for(int id:appWidgetIds) {
    Log.d(TAG, "onUpdate: ");

    Intent updateIntent = new Intent(context, RemoteService.class);
    Intent openIntent = new Intent(context,CourseView.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, openIntent, PendingIntent.FLAG_UPDATE_CURRENT);

    RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.app_widget);
    remoteViews.setRemoteAdapter(R.id.widget_lv, updateIntent);
    remoteViews.setOnClickPendingIntent(R.id.widget_tv,pendingIntent);
    remoteViews.setPendingIntentTemplate(R.id.widget_lv,pendingIntent);
    appWidgetManager.updateAppWidget(id, remoteViews);
}
super.onUpdate(context, appWidgetManager, appWidgetIds);
}

RemoteViewService && RemoteViewsFactory

  • RemoteViewService用于返回 RemoteViewsFactory,通常RemoteViewsFactory实现在RemoteViewService中即可
  • RemoteViewsFactory是ListView和StackView等子View的工厂,从这个角度来说它很像一种适配器,其中getViewAt方法相当于getView,但是需要返回的是一个RemoteView,其他方法可以参考ListView或RecycleView的适配器来写
  • 可以看到这个例子中调用了RemoteView的setOnClickFillInIntent方法,传入一个新的Intent,由于这里没有什么特殊的需求,所以没有对这个intent进行额外的操作,这个Intent和刚刚的PendingIntent模板组合就能启动Activity了 ,另外如果要实现View的onClickListener的效果则需要使用PendingIntent.getBroadcast来设置模板,并且在getViewAt创建的Intent中传入具体的ACTION,后面就和刚刚的UpdateService是一个套路了
public class RemoteService extends RemoteViewsService {


private static final String TAG = "RemoteService";


@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate: ");
}

@Override
public IBinder onBind(Intent intent) {
return super.onBind(intent);
}

@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new RemoteFactory();
}

public class RemoteFactory implements RemoteViewsService.RemoteViewsFactory {

public List<Integer> mDataList = new ArrayList<>();


public RemoteFactory() {
    for (int i = 0; i < 20; i++) {
        mDataList.add(i);
    }
    Log.d(TAG, "onCreate: ");
}

private static final String TAG = "RemoteFactory";

@Override
public void onCreate() {

}

@Override
public void onDataSetChanged() {

}

@Override
public void onDestroy() {

}

@Override
public int getCount() {
    return mDataList.size();
}

@Override
public RemoteViews getViewAt(int position) {

    if (position < 0 || position > mDataList.size())
        return null;

    Intent intent = new Intent();
    RemoteViews remoteViews = new RemoteViews(CurriculumApplication.getContext().getPackageName(), R.layout.item_lv_course);
    remoteViews.setTextViewText(R.id.widget_tv_course, mDataList.get(position) + "");
    remoteViews.setOnClickFillInIntent(R.id.item_lv_course,intent);

    return remoteViews;
}

@Override
public RemoteViews getLoadingView() {
    return null;
}

@Override
public int getViewTypeCount() {
    return 1;
}

@Override
public long getItemId(int position) {
    return position;
}

@Override
public boolean hasStableIds() {
    return true;
}

}
}
  • 最后注意非常重要的一点,既然RemoteViewService是Service,那么就必须注册,其中permission是一定要设置的,否则这个Service无法启动,这种情况下是不会有异常的,但是ListView等子控件将无法显示
<service
    android:name=".widget.RemoteService"
    android:permission="android.permission.BIND_REMOTEVIEWS"
    android:enabled="true"
    android:exported="true" >
</service>

RemoteViews

  • 现在,我们来研究RemoteView是怎么更新的,我们知道需要更新RemoteViews中的控件时,由于我们获取不到需要的View,所以应该通过RemoteViews的方法来实现更新,比如下面的setImageViewBitmap方法对应的就是ImageView的setBitmap方法
remoteViews.setImageViewBitmap()    
  • setImageViewBitmap调用setBitmap在第二个参数中传入了方法名,这个方法名是为了后面通过反射调用控件的方法而准备的,RemoteViews的Action表示我们想要对控件进行的操作,RemoteView会通过Binder传到Launcher的线程执行这些操作,从BitmapReflectionAction的名字很容易看出它使用了反射机制
public void setImageViewBitmap(int viewId, Bitmap bitmap) {
setBitmap(viewId, "setImageBitmap", bitmap);
}

public void setBitmap(int viewId, String methodName, Bitmap value) {
addAction(new BitmapReflectionAction(viewId, methodName, value));
}
  • 接着来看RemoteViews的apply方法,这个方法通过LayoutInflater获取到了我们的AppWidget的布局,有了布局就可以进行更新操作了
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;
}


private View inflateView(Context context, RemoteViews rv, ViewGroup parent) {
    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);
View v = inflater.inflate(rv.getLayoutId(), parent, false);
v.setTagInternal(R.id.widget_frame, rv.getLayoutId());
return v;
}
  • performApply遍历了所有的Action,执行其apply方法,在ReflectionAction中apply方法使用findViewById得到需要更新的布局,findViewById返回的是一个View,这里我们并不知道需要更新的View的具体类型,由于之前传入了方法名所以这里通过反射获取到了需要使用的方法并直接调用,理论上也可以通过使用instanceOf关键字然后再用类型转换来实现,实际setRemoteAdapter的apply方法就是这么做的,但是如果对于所有的View都使用instanceOf,RemoteViews支持的View一共有十几种,相比之下还是使用反射会更加简洁易维护
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);
    }
}
}   

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

AppWidget 与 binder

  • AppWidget是利用binder机制实现的,那么就来看看binder接口是怎么实现的,由于部分代码属于internal包,在Android Studio上可能无法查看,可以选择查看这里,如果有更方便的办法,请告诉我
  • 这里先讲解一下Luncher 是什么,为此首先明确一点,Android系统上所谓的桌面本身也是一个应用,这个系统应用我们管他叫启动器或者Luncher,更准确地说Luncher其实是一个Activity,我们的桌面的主视图是一个继承自PagedView的Workspace,每一页就是一个CellLayout,看看下面这一段,是不是觉得很熟悉?
public final class Launcher extends Activity implements View.OnClickListener, OnLongClickListener 
  • 我们知道Activity默认运行在Application指定的进程中,Launcher的Manifest文件我就懒得翻源码了,有需要可以看这里
  • 刚刚提到 AppWidgetManager 是在SystemServer进程中的,那么加上我们的自己应用的进程和Launcher所在的进程RemoteView就需要在三个进程之间传递,在一个应用中更新另一个应用的Activity,是不是很刺激?
  • 在需要更新AppWidget时,我们调用了AppWidgetManager.updateAppWidget方法,查看下列源码,调用到了mService的同名方法,而这个mService 的类型是IAppWidgetService,源码很长,我就不贴出来了,用过AIDL的应该会觉得很熟悉,和我们AS生成的 **AIDL.java的结构是一致的,mServie就是一个binder的代理(Proxy)类,调用它的方法会走Transaction过程,那么我们调用的方法最终就会在AppWidgetManager所在的进程中也就是SystemServer的进程中被调用了
public void updateAppWidget(int[] appWidgetIds, RemoteViews views) {
if (mService == null) {
    return;
}
try {
    mService.updateAppWidgetIds(mPackageName, appWidgetIds, views);
} catch (RemoteException e) {
    throw e.rethrowFromSystemServer();
}
}
  • 看完了Proxy,当然就轮到我们的Binder类了,Binder的实现类为AppWidgetServiceImpl,这里忽略一部分代码,这里主要是通过传入的id拿到了widget对象,widget对象是对每个appWidget的封装,结构很简单
private void updateAppWidgetIds(String callingPackage, int[] appWidgetIds,
    RemoteViews views, boolean partially) {
    ...
    synchronized (mLock) {
    ensureGroupStateLoadedLocked(userId);

    final int N = appWidgetIds.length;
    for (int i = 0; i < N; i++) {
        final int appWidgetId = appWidgetIds[i];

        // NOTE: The lookup is enforcing security across users by making
        // sure the caller can only access widgets it hosts or provides.
        Widget widget = lookupWidgetLocked(appWidgetId,
                Binder.getCallingUid(), callingPackage);

        if (widget != null) {
            updateAppWidgetInstanceLocked(widget, views, partially);
        }
    }
}
}


    private static final class Widget {
int appWidgetId;
int restoredId;  // tracking & remapping any restored state
Provider provider;
RemoteViews views;
Bundle options;
Host host;

@Override
public String toString() {
    return "AppWidgetId{" + appWidgetId + ':' + host + ':' + provider + '}';
}
}
  • 找到 Widget之后会调用 updateAppWidgetInstanceLocked来进行更新,这里将Widget进行拆解之后使用Handler了回到了主线程
    private void updateAppWidgetInstanceLocked(Widget widget, RemoteViews views,
    boolean isPartialUpdate) {
if (widget != null && widget.provider != null
        && !widget.provider.zombie && !widget.host.zombie) {

    if (isPartialUpdate && widget.views != null) {
        // For a partial update, we merge the new RemoteViews with the old.
        widget.views.mergeRemoteViews(views);
    } else {
        // For a full update we replace the RemoteViews completely.
        widget.views = views;
    }

    scheduleNotifyUpdateAppWidgetLocked(widget, views);
}
}


    private void scheduleNotifyUpdateAppWidgetLocked(Widget widget, RemoteViews updateViews) {
if (widget == null || widget.provider == null || widget.provider.zombie
        || widget.host.callbacks == null || widget.host.zombie) {
    return;
}

SomeArgs args = SomeArgs.obtain();
args.arg1 = widget.host;
args.arg2 = widget.host.callbacks;
args.arg3 = updateViews;
args.argi1 = widget.appWidgetId;

mCallbackHandler.obtainMessage(
        CallbackHandler.MSG_NOTIFY_UPDATE_APP_WIDGET,
        args).sendToTarget();
}
  • handleMessage中会调用到了handleNotifyUpdateAppWidget,这个callBack即是IAppWidgetHost,IAppWidgetHost又是一个Proxy类,这里我们就进入了另一个Binder机制了,这一个Binder用于与远程的Launcher进行通讯,同样地我们需要找到Binder的实现类
private void handleNotifyUpdateAppWidget(Host host, IAppWidgetHost callbacks,
    int appWidgetId, RemoteViews views) {
try {
    callbacks.updateAppWidget(appWidgetId, views);
} catch (RemoteException re) {
    synchronized (mLock) {
        Slog.e(TAG, "Widget host dead: " + host.id, re);
        host.callbacks = null;
    }
}
}
  • Launcher这个Activity中含有LauncherAppWidgetHost对象这个类继承自AppWidgetHost,AppWidgetHost中含有Binder的实现类Callbacks
public final class Launcher extends Activity
implements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks,
           View.OnTouchListener {
           ...
private LauncherAppWidgetHost mAppWidgetHost;
  • 这里使用Handler回到主线程,接下来自然是Launcher这个Activity要更新界面了
static class Callbacks extends IAppWidgetHost.Stub {
private final WeakReference<Handler> mWeakHandler;

public Callbacks(Handler handler) {
    mWeakHandler = new WeakReference<>(handler);
}

public void updateAppWidget(int appWidgetId, RemoteViews views) {
    if (isLocalBinder() && views != null) {
        views = views.clone();
    }
    Handler handler = mWeakHandler.get();
    if (handler == null) {
        return;
    }
    Message msg = handler.obtainMessage(HANDLE_UPDATE, appWidgetId, 0, views);
    msg.sendToTarget();
}
  • updateAppWidgetView中的applyRemoteViews其实就是调用RemoteViews的Apply方法,也就是我们刚刚提到的根据反射去得到View的具体方法然后调用,这样我们的界面就在Launcher上面更新了
void updateAppWidgetView(int appWidgetId, RemoteViews views) {
AppWidgetHostView v;
synchronized (mViews) {
    v = mViews.get(appWidgetId);
}
if (v != null) {
    v.updateAppWidget(views);
}
}


public void updateAppWidget(RemoteViews remoteViews) {
applyRemoteViews(remoteViews);
}