Fork me on GitHub

安卓之RemoteViews的学习

RemoteViews的学习

1.定义

  • RemoteViews是一种跨进程显示View的工具,它本身不是一个视图,只是一个描述类。
  • RemoteViews只是一个实现了Parcelable和Filter接口的类,而并非继承自View。
  • RemoteViews描述的远程视图需要通过layout资源文件定义,自定义布局。
  • RemoteViews类提供了一系列修改远程视图的方法用于跨进程更新界面。
  • RemoteViews在实际开发中,主要用于通知栏和桌面小部件的,本文以桌面小部件为例进行讲解。

2.AppWidget的使用

AppWidget是一种可选的界面,能够十分方便地为用户提供信息,更重要的是它为整个应用提供了快捷的入口。

2.1 定义小部件的界面

  • 前面提到RemoteViews只是显示View的工具,因此我们需要为小部件自定义一个视图布局,在res的layout文件夹中进行自定义布局

  • 此处需要注意有些View元素,RemoteViews是不支持的,下面会提到。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <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="horizontal">
    <TextView
    android:id="@+id/widget_tv"
    android:text="我是桌面小部件"
    android:textColor="#ffffff"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

    </LinearLayout>
  • 小部件的布局我只定义了一个用于展示的TextView。

2.2 定义小部件的配置信息(AppWidgetProviderInfo)

  • 在res新建一个xml的文件夹,新建一个xml文件,这个xml对应的便是AppWidgetProviderInfo类的类属性,包含了整个AppWidget的配置信息。
    <appwidget-provider>
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:initialLayout="@layout/my_appwidget"
        android:minHeight="120dp"
        android:minWidth="120dp"
        android:updatePeriodMillis="3000"
    </appwidget-provider>
    
  • 分析下AppWidgetProviderInfo的属性
    • initialLayout:小部件的初始化布局资源文件,只能包含RemoteViews允许的特定View元素。
    • minHeight,minWidth:定义的小部件的最小尺寸,其不是确切的宽高,小部件是位于有确定宽高的网格里面的,其尺寸的最小单位为桌面每个网格的尺寸,如果整数个网格数便向上取整,一般不超过4*4。
    • updatePeriodMillis:小部件的更新周期,以毫秒为单位,即更新的回调方法onUpdate的调用周期,更新会唤醒设备会有一定的能耗,因此不建议更新的频率太高。在jdk1.6之后,频率不能高于30分钟一次,否则会被设定为30分钟一次。
    • minResizeWidth,minResizeHeight:允许用户重新调整尺寸,minResizeWidth的大小可以小于minWidth,实际尺寸是不能小于minResizeWidth的。minResizeHeight同理。
    • resizeMode:表示可拉伸的方向,可选值:horizontal, vertical, none。
    • widgetCategory:home_screen,keyguard,指定widget是否在桌面和锁屏界面上显示。
    • initialKeyguardLayout : 锁屏界面的布局资源文件。

2.3 定义小部件的实现类(AppWidgetProvider)

  • 自定义一个继承AppWidgetProvider的类,重写自己的逻辑。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    public class AppWidget extends AppWidgetProvider{
    private static final String TAG = "AppWidget";
    private static final String CLICK_ACTION = "com.yuzeduan.CLICK";
    public static final String UPDATE_TEXT = "com.yuzeduan.UPDATE";

    @Override
    public void onReceive(Context context, Intent intent) {
    super.onReceive(context, intent);
    String action = intent.getAction();
    if(action.equals(UPDATE_TEXT)){
    Log.d(TAG, "onReceive: "+intent.getAction());
    String time = intent.getStringExtra("data");
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.my_appwidgt);
    remoteViews.setTextViewText(R.id.widget_tv, "启动服务更新第"+time+"次");
    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    ComponentName componentName = new ComponentName(context, AppWidget.class);
    // 更新appWidget
    appWidgetManager.updateAppWidget(componentName, remoteViews);
    }
    if(action.equals(CLICK_ACTION)){
    Log.d(TAG, "onReceive: "+intent.getAction());
    Intent serviceIntent = new Intent(context, MyIntentService.class);
    context.startService(serviceIntent);
    }
    }

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
    super.onUpdate(context, appWidgetManager, appWidgetIds);
    for(int id : appWidgetIds) {
    Log.d(TAG, "onUpdate: ");
    Intent intent = new Intent(CLICK_ACTION);
    PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
    // 小部件在Launcher桌面的布局
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.my_appwidgt);
    // 事件
    remoteViews.setOnClickPendingIntent(R.id.widget_tv, pendingIntent);
    // 更新AppWidget
    appWidgetManager.updateAppWidget(id, remoteViews);
    }
    }

    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
    super.onDeleted(context, appWidgetIds);
    Log.d(TAG, "onDeleted: ");
    }

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

    @Override
    public void onDisabled(Context context) {
    super.onDisabled(context);
    Log.d(TAG, "onDisabled: ");
    }
    }
  • 观察代码,是否觉得一丝的亲切,没错,AppWidgetProvider其实就是一个广播接收器,接下来分析一下他的几个重要方法。

    • onReceive:用于接收广播,其中有自定义的广播,还会接收诸如 APPWIDGET_UPDATE等Action的广播,然后调用对应的Widget的生命周期方法。(由onReceive的源码可看出)
    • onUpdate:该方法被调用的情况只有每个Widget被添加时和周期更新时,更新的时间由updatePeriodMillis影响,每个周期Widget会更新一次。
    • onDeleted:每删除一个桌面小部件都会调用一次。
    • onEnable:当该桌面小部件第一次被添加到桌面时候调用,再添加时候不调用,当移除所有的该桌面小部件后,再添加时候也会调用。
    • onDisabled:当最后一个该类型的桌面小部件被移除的时候,调用该方法。
  • 在自定义的广播发送方式上,本处是点击TextView发出一个点击的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyIntentService extends IntentService {
public MyIntentService() {
super("MyIntentService");
}

@Override
protected void onHandleIntent(@Nullable Intent intent) {
Update();
}

private void Update() {
Intent intent = new Intent(AppWidget.UPDATE_TEXT);
for (int i = 1; i<6; i++){
intent.putExtra("data", i+"");
sendBroadcast(intent);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
  • 使用了前台IntentService,用Service实现也可以,通过在异步线程中,发送广播。

  • 通过在AppWidgetProvider的onReceive进行接收广播后的逻辑,此时就要使用RemoteViews进行远程修改Widget中的控件的内容了。

2.4 在AndrodiManifest中声明小部件

  • 在上面便知道,看起来非常高大上的AppWidgetProvider其实就是一个广播接收器,根据广播接收器,静态注册需要在AndrodiManifest中声明,同理,AppWidgetProvider也需要,不过稍微有点特殊。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <receiver android:name=".AppWidget">
    <intent-filter>
    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    <action android:name="com.yuzeduan.CLICK" />
    <action android:name="com.yuzeduan.UPDATE" />
    </intent-filter>
    <meta-data
    android:name="android.appwidget.provider"
    android:resource="@xml/app_widget">
    </meta-data>
    </receiver>
  • 第一个action是作为小部件的标识必须存在的,如果不加,那么这个receiver便不是一个桌面小部件了。

  • 其他action是用于自定义广播而声明的。

  • 标签中的meta-data就是前面所说的AppWidgetProviderInfo,小部件的配置信息。

2.5 进阶使用

本例用ListView实现小部件的集合视图,即小部件的复杂布局实现

2.5.1 预备知识

  • 使用ListView的时候,首先想到的便是为其设置Adapter、填充数据和设置子项的点击事件。
  • AppWidget为ListView设置Adapter需要用到RemoteViewsService和RemoteViewsFactory,并在里面绑定数据,接下来会详细讲解。
  • 设置点击事件的时候,不能像之前一样setOnClickPendingIntent使用给简单的View设置单击事件一样,那样子开销太大,RemoteViews提供了setPendingIntentTemplate和setOnClickFillInIntent组合使用为复杂布局的子项设置点击事件。

2.5.2 RemoteViewsService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class RemoteService extends RemoteViewsService{
private String TAG = "RemoteService";

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

@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new RemoteFactory(this.getApplicationContext());
}
}
  • 继承RemoteViewsService,并重写返回我们实现的RemoteViewsFactory类实例。
  • RemoteViewsService是管理RemoteViews的服务,更确切的讲,是对ListView、GirdView等复杂视图的子项进行更新、管理的服务。

2.5.3 RemoteViewsFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class RemoteFactory implements RemoteViewsService.RemoteViewsFactory{
private String TAG = "RemoteFactory";
private Context mContext;
private List<String> mList = new ArrayList<>();

public RemoteFactory(Context context) {
mContext = context;
for(int i = 1; i < 6; i++){
mList.add("item"+i);
}
}

@Override
public void onCreate() {
}

@Override
public void onDataSetChanged() {
Log.d(TAG, "onDataSetChanged: "+mList.size());
}

@Override
public void onDestroy() {
mList.clear();
}

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

@Override
public RemoteViews getViewAt(int position) {
if (position < 0 || position >= mList.size())
return null;
String content = mList.get(position);
final RemoteViews rv = new RemoteViews(mContext.getPackageName(),
R.layout.item_widget_list);
rv.setTextViewText(R.id.item_tv, content);
Intent intent = new Intent();
intent.putExtra("content", content);
rv.setOnClickFillInIntent(R.id.item_tv, intent);

return rv;
}

@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;
}
}
  • RemoteFactory是继承自RemoteViewsService的RemoteViewsFactory的,是用于真正管理RemoteViews的复杂视图的,用于子项的数据绑定和展示,类似于ListView的BaseAdapter。
  • 其中有onCreated方法只在第一次创建Factory时候调用,同时添加多个小部件,它们其实是共用同一个Factory。
  • 最重要的是getViewAt方法,在这个方法中我们绑定了数据到子项中进行展示,并且使用setOnClickFillInIntent为每个子项进行设置点击事件。其他方法基本属于固定的实现。

2.5.4 具体使用

  • 建立layout资源文件的步骤我就不说了,说下在上面的AppWidgetProvider基础上添加的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
...
//复杂视图
Intent intent1 = new Intent(context, RemoteService.class);
remoteViews.setRemoteAdapter(R.id.widget_lv, intent1);
Intent itemClickIntent = new Intent(ITEM_CLICK);
PendingIntent pendingIntent1 = PendingIntent.getBroadcast(context, 0, itemClickIntent, 0);
remoteViews.setPendingIntentTemplate(R.id.widget_lv, pendingIntent1);
appWidgetManager.updateAppWidget(id, remoteViews);
}

public void onReceive(Context context, Intent intent) {
...
else if(action.equals(ITEM_CLICK)){
Log.d(TAG, "onReceive: "+intent.getAction());
Toast.makeText(context, "点击了"+intent.getStringExtra("content"), Toast.LENGTH_SHORT).show();
}
}
  • setRemoteAdapter方法通过传入带有服务的Intent和ListView的id,为ListView设置了适配器,绑定数据展示。
  • setPendingIntentTemplate相当为每个子项设置了一个模板的点击事件,再组合在RemoteViewFactory中的setOnClickFillInIntent方法,为特定的子项设置了点击事件。
  • onReceive方法没什么好说的,就是点击之后接收广播,弹出一个吐司。
1
2
3
4
5
<service
android:name=".RemoteService"
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="false">
</service>
  • 最后要在AndrodiManifest中添加RemoteViewsService的声明,以及添加的一些action。

2.6总结

  • 使用桌面小部件的一般过程
    • 自定义小部件的布局。
    • 使用XML文件为小部件提供元数据(AppWidgetProviderInfo,小部件的配置信息)。
    • 实现AppWidgetProvider,初始化小部件布局以及实现各个生命周期的逻辑。
    • 使用服务来更新界面。(不是必要的)
    • 若要实现复杂布局,则使用RemoteViewService和RemoteViewsFactory来实现。
    • 在AndrodiManifest中声明小部件。

3. RemoteViews支持的View类型

  • RemoteViews的作用是在其他进程中显示并更新View界面,但目前它并不支持所有的View类型,所支持的类型如下
    • layout:FrameLayout、LinearLayout、RelativeLayout、GridLayout。
    • View: AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper、ListView、GridView、StackView、AdapterViewFlipper、ViewStub。

4.走进RemoteViews的内部

4.1 构造方法

1
2
3
4
//UserHandle.myUserId())是当前进程的用户id
public RemoteViews(String packageName, int layoutId) {
this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
}
  • 在RemoteViews的构造方法方法中,传入应用的包名和自定义布局资源文件,通过应用程序的包名等信息可获得应用程序的资源。

4.2 setTextViewText

RemoteViews提供了许多setxxx方法供我们进行远程更新布局,此处以setTwxtViewText为例,其余类似。

1
2
3
4
5
6
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));
}
  • 可以看出,setTextViewText方法并没有真正的实现更新View的操作,只是生成了一个ReflectionAction对象,通过名字和传入的方法名参数,我们可以知道这应该是个反射类型的操作。接下来看下addAction的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Add an action to be executed on the remote side when apply is called.
* @param a The action to add
*/
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对象存在一个容器中,并未实现具体的更新操作。
  • 由注释可以知道,添加一个在Action,当apply方法调用时候,会在远程进行操作。那接下来就看看RemoteViews的apply方法到底是怎么个实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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) {
final Context contextForResources = getContextForResources(context);
Context inflationContext = new RemoteViewsContextWrapper(context, contextForResources);
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
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;
}
  • apply方法最主要的实现的其实是通过 LayoutInflater去加载RemoteViews的布局文件,有了布局,就可以实现更新操作了。
  • 上面的源码也没有关于View更新的操作,于是我们可以猜测,在performApply中应该会有突破口。接下来看下performApply方法
1
2
3
4
5
6
7
8
9
10
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);
}
}
}
  • 这个方法的源码比较清晰,就是遍历添加进mActions这个远程操作集合的每个Action,执行它的apply方法,于是,我们可以猜测,真正的View更新操作,应该是在Action的apply方法中。接下来,让我们进入这个我们期待已久的方法,去一探究竟。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Base class for the reflection actions.
*/
private final class ReflectionAction extends Action {
@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);
}
}
}
  • 至此,我们就可以看到ReflectionAction这个反射动作的apply方法,其实就是通过方法名获取对应的方法进行执行,因为RemoteViews所支持的View有十多种,此处如果用instanceof来判断的话,代码会很杂乱,采用反射可以使代码简单易看。

4.2 其他知识

  • RemoteViews中除了有apply方法之外还有一个reApply方法,这两个方法的区别在于前者会加载布局并更新界面,而后者只会更新界面。
  • 上面提到的只是setTextView方法,还有很多RemoteView提供的远程操作的方法,以及Action的子类,原理过程基本一致,只是有的不用反射实现。
  • RemoteViews在更新的操作中,封装了一个Action类,使得所有的View操作并不是立即更新的,而是在展示的时候才调用,同时,Action类是实现了Parcelable接口的,可以进行跨进程传递,这样减少了许多Binder接口,避免了大量的IPC操作。

5. AppWidget如何更新

前面讲了许许多多关于RemoteViews的内部机制,接下来就看下AppWidget是怎么跟RemoteViews进行联系的。

5.1 预备知识

  • 整个流程

  • 我们的桌面小部件是在桌面上显示的,因此我们需要了解下Luncher究竟是什么玩意。Laucher本身是安卓系统的一个应用,是系统启动后,运行的第一个应用。其中的Launcher类就是一个Activity。
  • 在上面使用桌面小部件的时候,我们可以知道AppWidgetProvider是通过AppWidgetManager来更新View的,而AppWidgetManager里面是有一个IAppWidgetService,用过AIDL的应该会了解,就是一个binder的代理(proxy)类。
  • AppWidgetProvider是运行在我们的自己的应用进程,而AppWidgetService是运行在SystemServer进程中,AppWidgetHost则是运行在显示Appwidget的进程中,例如:Laucher(桌面应用)。整个流程是跨越了三个进程的,也就是说RemoteViews需要在三个进程中之间传递,在另一个应用中更新另一个应用的Activity。

5.2 进入源码一探究竟

appWidgetManager.updateAppWidget(appwidgetids,remoteViews);

通过之前的桌面小部件的使用,我们不难发现,上面这行代码就是我们的突破口,于是进入updateAppWidget看下。

1
2
3
4
5
6
7
8
9
10
11
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);
}
}
  • 从上面的代码中可以看到,调用的是mService的一个方法,而mService就是我们上面提到的IAppWidgetService的一个实例,其初始化是在SystemServiceRegistry中的
1
2
3
4
5
6
7
8
9
10
11
12
13
registerService(Context.APPWIDGET_SERVICE, AppWidgetManager.class,
new CachedServiceFetcher<AppWidgetManager>() {
@Override
public AppWidgetManager createService(ContextImpl ctx) {
IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE);
return new AppWidgetManager(ctx, IAppWidgetService.Stub.asInterface(b));
}});

public AppWidgetManager(Context context, IAppWidgetService service) {
mPackageName = context.getOpPackageName();
mService = service;
mDisplayMetrics = context.getResources().getDisplayMetrics();
}
  • 注册服务的时候获取APPWIDGET_SERVICE ,之后的代码便是我们AIDL中常见的形式了,于是AppWidgetManager就持有了远程服务的binder代理类的引用了,调用的方法就是运行在远程进程中,即SystemSever。
  • IAppWidgetService的实现者是AppWidgetServiceImpl, 因此调用的是AppWidgetService的updateAppWidgetIds方法,于是下一步我们看下这个方法。
1
2
3
4
5
6
7
8
@Override
public void updateAppWidgetIds(String callingPackage, int[] appWidgetIds,
RemoteViews views) {
if (DEBUG) {
Slog.i(TAG, "updateAppWidgetIds() " + UserHandle.getCallingUserId());
}
updateAppWidgetIds(callingPackage, appWidgetIds, views, false);
}
  • 这个方法只是调用四个参数的重载方法,于是看下它的重载方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//贴出部分代码
final int bitmapMemoryUsage = (views != null) ? views.estimateMemoryUsage() : 0;
if (bitmapMemoryUsage > mMaxWidgetBitmapMemory) {
throw new IllegalArgumentException("RemoteViews for widget update exceeds"
+ " maximum bitmap memory usage (used: " + bitmapMemoryUsage
+ ", max: " + mMaxWidgetBitmapMemory + ")");
}

synchronized (mLock) {
ensureGroupStateLoadedLocked(userId);
final int N = appWidgetIds.length;
for (int i = 0; i < N; i++) {
final int appWidgetId = appWidgetIds[i];
Widget widget = lookupWidgetLocked(appWidgetId,
Binder.getCallingUid(), callingPackage);
if (widget != null) {
updateAppWidgetInstanceLocked(widget, views, partially);
}
}
}
  • 这个方法会进行图片大小的限制的检查,其重要的部分是,遍历每个appWidgetId,通过lookupWidgetLocked获取到其对应的widget,再调用updateAppWidgetInstanceLocked方法,接下来让我们继续看。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private Widget lookupWidgetLocked(int appWidgetId, int uid, String packageName) {
final int N = mWidgets.size();
for (int i = 0; i < N; i++) {
Widget widget = mWidgets.get(i);
if (widget.appWidgetId == appWidgetId
&& mSecurityPolicy.canAccessAppWidget(widget, uid, packageName)) {
return widget;
}
}
return null;
}

private static final class Widget {
int appWidgetId;
int restoredId; // tracking & remapping any restored state
Provider provider; // 对应AppWidgetProvider,里面有AppWidgetProvider信息。
RemoteViews views; //表示View的RemoteView
Bundle options;
Host host; //显示的地方

@Override
public String toString() {
return "AppWidgetId{" + appWidgetId + ':' + host + ':' + provider + '}';
}
}
  • Widget是AppWidgetServiceImpl的一个静态内部类,其包含了很多appWidget的信息。
  • lookupWidgetLocked只是从mWidgets取出Widget,此时我们会有一个困惑,我们是什么时候添加Widget到这个容器中的。主要有三个地方,一个是绑定AppWidgetProvider跟id时,初始化时加载AppWidget与对应的host;一个是第一次添加AppWidget到桌面时,给AppWidget分配id的时候;一个是restore AppWidget的时候。我们可以看下分配id的部分代码。
1
2
3
4
5
6
7
8
9
10
final int appWidgetId = incrementAndGetAppWidgetIdLocked(userId); //增量分配一个id,保证不冲突
HostId id = new HostId(Binder.getCallingUid(), hostId, callingPackage); //得到hostid
Host host = lookupOrAddHostLocked(id); //根据id获取host
Widget widget = new Widget();
widget.appWidgetId = appWidgetId;
widget.host = host;
host.widgets.add(widget); //把widget添加到host的widgets列表中
addWidgetLocked(widget); //添加
saveGroupStateAsync(userId);
return appWidgetId;
  • 可以看见,每次分配id,都会new出一个Widget对象,并设置Widget的属性,其中host是有关小部件显示的界面的,下面会讲解,这里先有个印象,分配id的时候,就会把Widget对象添加进显示界面的小部件容器中了。
  • 实际上这里并没有添加对应的Provider,因此当我们在Laucher开发的时候,先调用allocateAppWidgetId方法,然后调用bindAppWidgetId方法绑定id与AppWidgetProvider。
  • 接下来继续回来看updateAppWidgetInstanceLocked方法,看找到Widget之后,会做什么事情。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private void updateAppWidgetInstanceLocked(Widget widget, RemoteViews views,
boolean isPartialUpdate) {
if (widget != null && widget.provider != null
&& !widget.provider.zombie && !widget.host.zombie) { // 保证widget有效,并且host也有效
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; //callbacks 是host的跨进程调用接口,来自于startListening

args.arg3 = updateViews;
args.argi1 = widget.appWidgetId;

mCallbackHandler.obtainMessage(
CallbackHandler.MSG_NOTIFY_UPDATE_APP_WIDGET,
args).sendToTarget();
}
  • 这两个方法的实现,其实将Widget进行分解,然后用handler机制,发送信息,使得主线程中的handler的handleMessage得以调用,因此我们接下来就要看下handleMessage的实现了。
1
2
3
4
5
6
7
8
9
10
11
12
13
//贴出主要代码
@Override
public void handleMessage(Message message) {
switch (message.what) {
case MSG_NOTIFY_UPDATE_APP_WIDGET: {
SomeArgs args = (SomeArgs) message.obj;
Host host = (Host) args.arg1;
IAppWidgetHost callbacks = (IAppWidgetHost) args.arg2;
RemoteViews views = (RemoteViews) args.arg3;
final int appWidgetId = args.argi1;
args.recycle();
handleNotifyUpdateAppWidget(host, callbacks, appWidgetId, views);
} break;
  • 其实处理了这么多,就是要执行handleNotifyUpdateAppWidget,此处出现了IAppWidgetHost,于是我们第二个跨进程的过程就要出现了。先让我们看下handleNotifyUpdateAppWidget方法的实现。
1
2
3
4
5
6
7
8
9
10
11
private void handleNotifyUpdateAppWidget(Host host, IAppWidgetHost callbacks,
int appWidgetId, RemoteViews views) {
try {
callbacks.updateAppWidget(appWidgetId, views); //通知AppWidgtHost
} catch (RemoteException re) {
synchronized (mLock) {
Slog.e(TAG, "Widget host dead: " + host.id, re);
host.callbacks = null;
}
}
}
  • 此处调用了callbacks的方法,因此我们这就来看下IAppWidgetHost是个什么玩意。
  • 根据我们前面的IAppWidgetService,到现在的IAppWidgetHost,我们可以知道,这又是一个代理类,那又是一个跨进程的实现了,于是我们又进入了另一个Binder机制了,这个Binder就是用于和远程的Lancher进行通讯的。在讲Binder的实现类时,先看下AppWidgetServiceImp怎么持有callback引用的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Host(Launcher应用)的入口方法
public void startListening() {
int[] updatedIds;
ArrayList<RemoteViews> updatedViews = new ArrayList<RemoteViews>();
try {
updatedIds = sService.startListening(mCallbacks, mContextOpPackageName, mHostId,
updatedViews); //把AppWidgetHost端的mCallbacks传递给AppWidgetService,mCallbacks 是Binder对象。
}
catch (RemoteException e) {
throw new RuntimeException("system server dead?", e);
}

final int N = updatedIds.length;
for (int i = 0; i < N; i++) {
updateAppWidgetView(updatedIds[i], updatedViews.get(i)); //初始化小部件
}
}

//AppWidgetServiceImp的实现
@Override
public int[] startListening(IAppWidgetHost callbacks, String callingPackage,
int hostId, List<RemoteViews> updatedViews) {
....
HostId id = new HostId(Binder.getCallingUid(), hostId, callingPackage);
Host host = lookupOrAddHostLocked(id);

host.callbacks = callbacks; //设置callbacks
....
}
}
  • 从Host端的调用来看,是进行跨进程调用,持有sService,调用它的startListening,传入Callbacks对象,在AppWidgetServiceImp端,将该Callbacks对象赋给对应的widget的host。
  • 接下来让我们看下这个BInder的实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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();
}
}
  • Binder类的实现源码很清晰,远程调用的updateAppWidget方法,使得进程从SystemSever转换到了Laucher所在的进程,于是接下来就要执行我们展示桌面小部件的真正环节了。
  • updateAppWidget方法中持有Handler对象,AppWidgetService进程的调用该方法时,Handler就会发出消息,接下来我们就来看下handleMessage方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class UpdateHandler extends Handler {
public UpdateHandler(Looper looper) {
super(looper);
}

public void handleMessage(Message msg) {
switch (msg.what) {
case HANDLE_UPDATE: {
updateAppWidgetView(msg.arg1, (RemoteViews)msg.obj);
break;
}
}
}
}
  • 接下来调用AppWidgetHost的updateAppWidgetView方法
1
2
3
4
5
6
7
8
9
void updateAppWidgetView(int appWidgetId, RemoteViews views) {
AppWidgetHostView v;
synchronized (mViews) {
v = mViews.get(appWidgetId);
}
if (v != null) {
v.updateAppWidget(views);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public void updateAppWidget(RemoteViews remoteViews) {

boolean recycled = false;
View content = null;
Exception exception = null;

if (remoteViews == null) {
if (mViewMode == VIEW_MODE_DEFAULT) {
// We've already done this -- nothing to do.
return;
}
content = getDefaultView(); // 默认的View
mLayoutId = -1;
mViewMode = VIEW_MODE_DEFAULT;
} else {
mRemoteContext = getRemoteContext();
int layoutId = remoteViews.getLayoutId();
if (content == null && layoutId == mLayoutId) {
try {
remoteViews.reapply(mContext, mView, mOnClickHandler);
content = mView;
recycled = true;
if (LOGD) Log.d(TAG, "was able to recycled existing layout");
} catch (RuntimeException e) {
exception = e;
}
}

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;
}
}
mLayoutId = layoutId;
mViewMode = VIEW_MODE_CONTENT;
}
if (content == null) {
if (mViewMode == VIEW_MODE_ERROR) {
return ;
}
Log.w(TAG, "updateAppWidget couldn't find any view, using error view", exception);
content = getErrorView(); // 在失败的情况下,会使用ErrorView。
mViewMode = VIEW_MODE_ERROR;
}

if (!recycled) {
prepareView(content);
addView(content);
}

if (mView != content) {
removeView(mView);
mView = content;
}

if (CROSSFADE) {
if (mFadeStartTime < 0) {
mFadeStartTime = SystemClock.uptimeMillis();
invalidate();
}
}
}
  • 终于看到我们想要东西了,在AppWidgetHostView的updateAppWidget中,我们看到了前面分析RemoteViews时候提到的apply方法,也就是与RemoteViews关联上了,RemoteViews中的自定义布局可以在Laucher中进行展示了。
  • 分析上面的方法,我们还可以发现桌面小部件的View展示的一些情况
    • 如果remoteView为空,则看是否已经使用了默认视图,如果已经使用了直接返回,如果没有则使用默认的视图
    • 如果remoteView不为空,则看layoutid是否跟现在已经使用的视图的layoutid一致,一致则重用旧的视图,调用remoteView.reapply方法重用视图,并且设置视图内容。
    • 如果layoutid跟现使用的视图不一致,则调用remoteView.apply方法得到新的视图。
    • 如果上面的步骤都没有得到视图,则使用错误视图。
    • 如果新的视图与旧的视图不一致,则添加新的视图,删除旧的视图。

5.3 总结

  • RemoteViews在AppWidget的更新中,充当的是一个显示View的工具,它携带了自定义的View布局和更新的操作(Action)在三个进程之间进行传递。
  • 整个桌面小部件的更新过程中,跨越的进程有自己本身的应用进程,SystemSever进程,Laucher的应用进程,并在Laucher的应用进程中进行最终的更新和展示。
  • 三个进程中,SystemSever充当了连接Laucher进程和本身应用进程的角色,接收app进程提交更新的消息之后传递给laucher进程。
-------------本文结束感谢您的阅读-------------

本文标题:安卓之RemoteViews的学习

文章作者:AllenYu

发布时间:2018年11月25日 - 17:11

最后更新:2018年11月25日 - 20:11

原始链接:http://yuzeduan.github.io/2018/11/25/安卓之RemoteViews的学习/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。