Android RemoteViews 实现原理
一、RemoteViews 是什么
核心问题:App A 想在 App B 的进程中显示自己的 UI
场景1: Widget → App 的 UI 显示在 Launcher 进程中
场景2: Notification → App 的 UI 显示在 SystemUI 进程中
场景3: 自定义通知 → App 的自定义布局显示在 SystemUI 进程中
直接传 View 对象?
❌ View 持有 Context、Handler、大量引用,无法序列化
❌ View 不是 Parcelable
直接传 Bitmap?
❌ 太大,Binder 传不了
❌ 没有交互能力(不能点击)
★ RemoteViews 的方案:
不传 View,也不传 Bitmap
只传 "布局ID + 操作指令列表"
在目标进程中重建 View 并回放操作
┌──── App 进程 ────────────────────┐ ┌──── Launcher/SystemUI 进程 ────┐
│ │ │ │
│ RemoteViews rv = new RemoteViews │ │ │
│ (packageName, R.layout.widget) │ │ │
│ │ │ │
│ rv.setTextViewText( │ │ │
│ R.id.title, "Hello") │ │ │
│ rv.setImageViewResource( │ │ │
│ R.id.icon, R.drawable.ic) │ │ │
│ rv.setOnClickPendingIntent( │ │ │
│ R.id.btn, pendingIntent) │ │ │
│ │ │ │
│ 实际存储的是: │ │ │
│ ┌─ Action 列表 ────────────┐ │ │ │
│ │ Action1: setText(id,val) │ │ │ │
│ │ Action2: setImage(id,res)│ │ │ │
│ │ Action3: setClick(id,PI) │ │ │ │
│ └──────────────────────────┘ │ │ │
│ │ │ │
│ ════════ Binder 传输 ═══════════════▶ │ │
│ (序列化: 布局ID + Action列表) │ │ RemoteViews.apply(context) │
│ │ │ ① inflate 布局 XML │
│ │ │ ② 逐个执行 Action: │
│ │ │ setText → tv.setText() │
│ │ │ setImage → iv.setImageRes()│
│ │ │ setClick → v.setOnClick() │
│ │ │ ③ 返回构建好的 View │
│ │ │ │
└───────────────────────────────────┘ └────────────────────────────────┘
二、核心设计:Action 模式
2.1 整体架构
RemoteViews
├── String mPackage
├── int mLayoutId
├── List<Action> mActions
│
├── setTextViewText() → 添加 ReflectionAction
├── setImageViewResource() → 添加 ReflectionAction
├── setImageViewBitmap() → 添加 BitmapReflectionAction
├── setViewVisibility() → 添加 ReflectionAction
├── setOnClickPendingIntent()→ 添加 SetOnClickPendingIntent
├── setInt() → 添加 ReflectionAction
├── setBundle() → 添加 ReflectionAction
├── ...
│
├── writeToParcel()
├── apply()
└── reapply()
2.2 Action 基类
public class RemoteViews implements Parcelable {
private String mPackage;
private int mLayoutId;
private ArrayList<Action> mActions;
private abstract static class Action implements Parcelable {
int viewId;
public abstract void apply(View root, ViewGroup rootParent,
ActionApplyParams params) throws ActionException;
public abstract int getActionTag();
}
}
2.3 核心 Action 子类
private static class ReflectionAction extends Action {
String methodName;
int type;
Object value;
@Override
public void apply(View root, ViewGroup rootParent,
ActionApplyParams params) {
View target = root.findViewById(viewId);
Class<?> paramType = getParameterType(type);
Method method = target.getClass()
.getMethod(methodName, paramType);
method.invoke(target, value);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(getActionTag());
dest.writeInt(viewId);
dest.writeString(methodName);
dest.writeInt(type);
writeValue(dest, type, value);
}
}
private static class BitmapReflectionAction extends Action {
Bitmap bitmap;
String methodName;
@Override
public void apply(View root, ViewGroup rootParent,
ActionApplyParams params) {
View target = root.findViewById(viewId);
Method method = target.getClass()
.getMethod(methodName, Bitmap.class);
method.invoke(target, bitmap);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(getActionTag());
dest.writeInt(viewId);
dest.writeString(methodName);
bitmap.writeToParcel(dest, flags);
}
}
private static class SetOnClickPendingIntent extends Action {
PendingIntent pendingIntent;
@Override
public void apply(View root, ViewGroup rootParent,
ActionApplyParams params) {
View target = root.findViewById(viewId);
target.setOnClickListener(v -> {
try {
pendingIntent.send();
} catch (PendingIntent.CanceledException e) {
}
});
}
}
private static class SetRemoteViewsAdapterIntent extends Action {
Intent intent;
@Override
public void apply(View root, ViewGroup rootParent,
ActionApplyParams params) {
AdapterView<?> target = (AdapterView<?>) root.findViewById(viewId);
target.setRemoteViewsAdapter(intent);
}
}
private static class ViewGroupActionAdd extends Action {
RemoteViews mNestedViews;
@Override
public void apply(View root, ...) {
ViewGroup target = (ViewGroup) root.findViewById(viewId);
View nestedView = mNestedViews.apply(context, target);
target.addView(nestedView);
}
}
三、RemoteViews 的 API 与对应 Action
public class RemoteViews implements Parcelable {
public void setTextViewText(int viewId, CharSequence text) {
addAction(new ReflectionAction(viewId, "setText",
ReflectionAction.CHAR_SEQUENCE, text));
}
public void setTextColor(int viewId, int color) {
addAction(new ReflectionAction(viewId, "setTextColor",
ReflectionAction.INT, color));
}
public void setTextViewTextSize(int viewId, int units, float size) {
addAction(new TextViewSizeAction(viewId, units, size));
}
public void setImageViewResource(int viewId, int srcId) {
addAction(new ReflectionAction(viewId, "setImageResource",
ReflectionAction.INT, srcId));
}
public void setImageViewBitmap(int viewId, Bitmap bitmap) {
addAction(new BitmapReflectionAction(viewId,
"setImageBitmap", bitmap));
}
public void setImageViewIcon(int viewId, Icon icon) {
addAction(new ReflectionAction(viewId, "setImageIcon",
ReflectionAction.ICON, icon));
}
public void setImageViewUri(int viewId, Uri uri) {
addAction(new ReflectionAction(viewId, "setImageURI",
ReflectionAction.URI, uri));
}
public void setViewVisibility(int viewId, int visibility) {
addAction(new ReflectionAction(viewId, "setVisibility",
ReflectionAction.INT, visibility));
}
public void setInt(int viewId, String methodName, int value) {
addAction(new ReflectionAction(viewId, methodName,
ReflectionAction.INT, value));
}
public void setBoolean(int viewId, String methodName, boolean value) {
addAction(new ReflectionAction(viewId, methodName,
ReflectionAction.BOOLEAN, value));
}
public void setString(int viewId, String methodName, String value) {
addAction(new ReflectionAction(viewId, methodName,
ReflectionAction.STRING, value));
}
public void setOnClickPendingIntent(int viewId, PendingIntent pi) {
addAction(new SetOnClickPendingIntent(viewId, pi));
}
public void setOnClickFillInIntent(int viewId, Intent fillInIntent) {
addAction(new SetOnClickFillInIntent(viewId, fillInIntent));
}
public void setPendingIntentTemplate(int viewId, PendingIntent pi) {
addAction(new SetPendingIntentTemplate(viewId, pi));
}
public void setProgressBar(int viewId, int max,
int progress, boolean indeterminate) {
addAction(new ReflectionAction(viewId, "setMax",
ReflectionAction.INT, max));
addAction(new ReflectionAction(viewId, "setProgress",
ReflectionAction.INT, progress));
addAction(new ReflectionAction(viewId, "setIndeterminate",
ReflectionAction.BOOLEAN, indeterminate));
}
public void addView(int viewId, RemoteViews nestedView) {
addAction(new ViewGroupActionAdd(viewId, nestedView));
}
public void removeAllViews(int viewId) {
addAction(new ViewGroupActionRemove(viewId));
}
public void setRemoteAdapter(int viewId, Intent intent) {
addAction(new SetRemoteViewsAdapterIntent(viewId, intent));
}
private void addAction(Action a) {
if (mActions == null) {
mActions = new ArrayList<>();
}
mActions.add(a);
}
}
四、序列化与反序列化(跨进程传输)
4.1 writeToParcel
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(MODE_NORMAL);
mApplication.writeToParcel(dest, flags);
dest.writeInt(mLayoutId);
if (mActions != null) {
int count = mActions.size();
dest.writeInt(count);
for (int i = 0; i < count; i++) {
Action a = mActions.get(i);
dest.writeInt(a.getActionTag());
a.writeToParcel(dest, flags);
}
} else {
dest.writeInt(0);
}
}
4.2 反序列化 (createFromParcel)
public RemoteViews(Parcel parcel) {
mApplication = ApplicationInfo.CREATOR.createFromParcel(parcel);
mLayoutId = parcel.readInt();
int count = parcel.readInt();
mActions = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
int tag = parcel.readInt();
Action action = getActionFromParcel(tag, parcel);
mActions.add(action);
}
}
private static Action getActionFromParcel(int tag, Parcel parcel) {
switch (tag) {
case SET_ON_CLICK_PENDING_INTENT_TAG:
return new SetOnClickPendingIntent(parcel);
case REFLECTION_ACTION_TAG:
return new ReflectionAction(parcel);
case BITMAP_REFLECTION_ACTION_TAG:
return new BitmapReflectionAction(parcel);
case VIEW_GROUP_ACTION_ADD_TAG:
return new ViewGroupActionAdd(parcel);
case VIEW_GROUP_ACTION_REMOVE_TAG:
return new ViewGroupActionRemove(parcel);
case SET_PENDING_INTENT_TEMPLATE_TAG:
return new SetPendingIntentTemplate(parcel);
case SET_REMOTE_VIEW_ADAPTER_INTENT_TAG:
return new SetRemoteViewsAdapterIntent(parcel);
default:
throw new ActionException("Unknown action tag: " + tag);
}
}
序列化后的 Parcel 数据结构:
┌────────────────────────────────────────────────────────┐
│ Mode: MODE_NORMAL (int) │
├────────────────────────────────────────────────────────┤
│ ApplicationInfo: │
│ packageName: "com.example.app" (String) │
│ uid: 10086 (int) │
├────────────────────────────────────────────────────────┤
│ LayoutId: 0x7f0a0001 (int) ← R.layout.widget_layout │
├────────────────────────────────────────────────────────┤
│ ActionCount: 3 (int) │
├────────────────────────────────────────────────────────┤
│ Action[0]: │
│ tag: REFLECTION_ACTION_TAG (int) │
│ viewId: 0x7f080001 (int) ← R.id.title │
│ methodName: "setText" (String) │
│ type: CHAR_SEQUENCE (int) │
│ value: "Hello World" (CharSequence) │
├────────────────────────────────────────────────────────┤
│ Action[1]: │
│ tag: REFLECTION_ACTION_TAG │
│ viewId: 0x7f080002 ← R.id.icon │
│ methodName: "setImageResource" │
│ type: INT │
│ value: 0x7f060001 ← R.drawable.ic_weather │
├────────────────────────────────────────────────────────┤
│ Action[2]: │
│ tag: SET_ON_CLICK_PENDING_INTENT_TAG │
│ viewId: 0x7f080003 ← R.id.button │
│ pendingIntent: PendingIntent (Parcelable) │
└────────────────────────────────────────────────────────┘
总大小:通常只有几百字节到几 KB(除非包含 Bitmap)
远小于传输整个 View 树!
五、apply() —— 在目标进程中构建 View ★★★
5.1 apply() 完整流程
public View apply(Context context, ViewGroup parent) {
return apply(context, parent, null );
}
public View apply(Context context, ViewGroup parent,
OnClickHandler handler) {
Context remoteContext = getContextForResources(context);
LayoutInflater inflater = LayoutInflater.from(context);
inflater = inflater.cloneInContext(remoteContext);
View result = inflater.inflate(mLayoutId, parent, false);
performApply(result, parent, params);
return result;
}
private void performApply(View v, ViewGroup parent,
ActionApplyParams params) {
if (mActions != null) {
for (Action a : mActions) {
a.apply(v, parent, params);
}
}
}
private Context getContextForResources(Context context) {
try {
return context.createPackageContext(
mApplication.packageName,
Context.CONTEXT_RESTRICTED);
} catch (PackageManager.NameNotFoundException e) {
return context;
}
}
5.2 reapply() —— 增量更新
public void reapply(Context context, View v) {
performApply(v, (ViewGroup) v.getParent(), params);
}
5.3 createPackageContext 的作用
为什么需要 createPackageContext?
问题:
Widget 布局在 App 的 APK 中 (R.layout.widget_layout)
但 Widget 要显示在 Launcher 进程中
Launcher 的 Resources 里没有 App 的资源!
解决:
通过 createPackageContext("com.example.app")
创建一个能访问 App 资源的 Context
┌─── App APK ────────────────────────┐
│ res/layout/widget_layout.xml │ ← App 的布局
│ res/drawable/ic_weather.png │ ← App 的图片
│ res/values/strings.xml │ ← App 的字符串
└─────────────────────────────────────┘
↑
│ createPackageContext("com.example.app")
│ → 创建一个 Context,其 Resources 指向 App 的 APK
│ → inflater 使用这个 Context 来加载 App 的布局
│
┌─── Launcher 进程 ───────────────────┐
│ │
│ remoteContext = createPackageContext │
│ inflater.cloneInContext(remoteCxt) │
│ inflater.inflate(mLayoutId, ...) │
│ → 成功加载 App APK 中的布局! │
│ │
└──────────────────────────────────────┘
六、在 Widget 中的应用
6.1 Widget 完整工作流程
┌──── App 进程 ───────────────────────────────────────────────┐
│ │
│ AppWidgetProvider (BroadcastReceiver) │
│ │ │
│ ├── onUpdate(context, appWidgetManager, appWidgetIds) │
│ │ │ │
│ │ │ ① 创建 RemoteViews │
│ │ │ RemoteViews views = new RemoteViews( │
│ │ │ getPackageName(), │
│ │ │ R.layout.widget_layout); │
│ │ │ │
│ │ │ ② 设置内容(记录 Action) │
│ │ │ views.setTextViewText(R.id.temp, "25°C"); │
│ │ │ views.setImageViewResource(R.id.icon, R.drawable); │
│ │ │ views.setOnClickPendingIntent(R.id.btn, pi); │
│ │ │ │
│ │ │ ③ 更新 Widget │
│ │ │ appWidgetManager.updateAppWidget(widgetId, views); │
│ │ │ │ │
│ │ │ │ Binder IPC │
│ │ │ ↓ │
│ │ │ AppWidgetServiceImpl (SystemServer) │
│ │ │ │ 存储 RemoteViews │
│ │ │ │ 通知 AppWidgetHost (Launcher) │
│ │ │ ↓ │
└──│─────│─────────────────────────────────────────────────────┘
│ │
│ │ Binder IPC
│ ↓
┌──── Launcher 进程 ──────────────────────────────────────────┐
│ │
│ AppWidgetHost │
│ │ │
│ ├── onProviderChanged / updateAppWidgetView │
│ │ │ │
│ │ │ ④ 接收 RemoteViews (反序列化) │
│ │ │ │
│ │ │ ⑤ 构建 View │
│ │ │ if (首次) { │
│ │ │ View v = remoteViews.apply(context, parent); │
│ │ │
│ │ │ hostView.addView(v); │
│ │ │ } else { │
│ │ │ remoteViews.reapply(context, existingView); │
│ │ │
│ │ │ } │
│ │ │ │
│ │ │ ⑥ 用户点击按钮 │
│ │ │ → PendingIntent.send() │
│ │ │ → 跨进程回调到 App │
│ │ │ │
└──────────────────────────────────────────────────────────────┘
6.2 AppWidgetHostView
public class AppWidgetHostView extends FrameLayout {
private RemoteViews mRemoteViews;
private View mView;
public void updateAppWidget(RemoteViews remoteViews) {
mRemoteViews = remoteViews;
if (remoteViews == null) {
showDefaultView();
return;
}
boolean recycled = false;
if (mView != null && canRecycleView(mView)) {
try {
remoteViews.reapply(mContext, mView);
recycled = true;
} catch (RuntimeException e) {
}
}
if (!recycled) {
try {
mView = remoteViews.apply(mContext, this);
} catch (RuntimeException e) {
showErrorView();
return;
}
removeAllViews();
addView(mView);
}
}
private boolean canRecycleView(View v) {
return mRemoteViews != null
&& mRemoteViews.getLayoutId() == mLastLayoutId;
}
}
七、在 Notification 中的应用
7.1 通知使用 RemoteViews
Notification notification = new Notification.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notif)
.setContentTitle("Title")
.setContentText("Content")
.build();
RemoteViews customView = new RemoteViews(
getPackageName(), R.layout.custom_notification);
customView.setTextViewText(R.id.title, "Custom Title");
customView.setImageViewBitmap(R.id.album_art, albumBitmap);
customView.setOnClickPendingIntent(R.id.play_btn, playPendingIntent);
Notification notification = new Notification.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notif)
.setCustomContentView(customView)
.setCustomBigContentView(expandedView)
.setCustomHeadsUpContentView(headsUpView)
.build();
7.2 通知中的 RemoteViews 传递流程
App 进程 SystemServer SystemUI 进程
│ │ │
│ notify(notification) │ │
│ ──── Binder ─────────→│ │
│ │ │
│ Notification 序列化: │ │
│ ├── smallIcon │ │
│ ├── contentView │ NMS 存储 │
│ │ (RemoteViews) │ │
│ └── Actions 列表 │ │
│ │ │
│ │ onNotificationPosted() │
│ │ ──── Binder ────────────→ │
│ │ │
│ │ 反序列化 Notification │
│ │ 获取 RemoteViews │
│ │ │
│ │ ★ inflate + apply │
│ │ remoteContext = │
│ │ createPackageContext( │
│ │ "com.example.app") │
│ │ View = rv.apply(ctx, p) │
│ │ │
│ │ 添加到通知列表 │
│ │ │
八、安全限制
8.1 布局限制
FrameLayout
LinearLayout
RelativeLayout
GridLayout
TextView
ImageView
Button
ImageButton
ProgressBar
Chronometer
AnalogClock
ViewFlipper
ListView
GridView
StackView
AdapterViewFlipper
ViewStub
EditText
WebView
SurfaceView
RecyclerView
自定义 View
private View inflateView(Context context, RemoteViews rv,
ViewGroup parent) {
if (!view.getClass().isAnnotationPresent(RemoteView.class)) {
throw new ActionException("Cannot use " +
view.getClass().getName() + " in RemoteViews");
}
}
@RemoteView
public class TextView extends View { ... }
@RemoteView
public class ImageView extends View { ... }
@RemoteView
public class ProgressBar extends View { ... }
8.2 反射安全限制
@Override
public void apply(View root, ViewGroup rootParent, ...) {
View target = root.findViewById(viewId);
Method method = target.getClass().getMethod(methodName, paramType);
if (!method.isAnnotationPresent(RemotableViewMethod.class)) {
throw new ActionException("Cannot call method " +
methodName + " - not annotated with @RemotableViewMethod");
}
method.invoke(target, value);
}
public class TextView extends View {
@RemotableViewMethod
public void setText(CharSequence text) { ... }
@RemotableViewMethod
public void setTextColor(int color) { ... }
}
九、列表 Widget 的特殊处理
public class MyWidgetService extends RemoteViewsService {
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new MyRemoteViewsFactory(this, intent);
}
}
public class MyRemoteViewsFactory implements RemoteViewsFactory {
private List<String> mItems;
@Override
public void onCreate() {
mItems = loadData();
}
@Override
public int getCount() {
return mItems.size();
}
@Override
public RemoteViews getViewAt(int position) {
RemoteViews rv = new RemoteViews(
mContext.getPackageName(),
R.layout.widget_list_item);
rv.setTextViewText(R.id.item_text, mItems.get(position));
Intent fillInIntent = new Intent();
fillInIntent.putExtra("item_position", position);
rv.setOnClickFillInIntent(R.id.item_text, fillInIntent);
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; }
@Override public void onDataSetChanged() { mItems = loadData(); }
@Override public void onDestroy() { }
}
@Override
public void onUpdate(Context context, AppWidgetManager manager, int[] ids) {
RemoteViews views = new RemoteViews(
context.getPackageName(), R.layout.widget_list);
Intent serviceIntent = new Intent(context, MyWidgetService.class);
views.setRemoteAdapter(R.id.list_view, serviceIntent);
Intent clickIntent = new Intent(context, MyActivity.class);
PendingIntent pi = PendingIntent.getActivity(context, 0, clickIntent, 0);
views.setPendingIntentTemplate(R.id.list_view, pi);
views.setEmptyView(R.id.list_view, R.id.empty_view);
manager.updateAppWidget(ids, views);
}
列表 Widget 的跨进程交互:
Launcher 进程 App 进程
│ │
│ 需要显示第 N 项 │
│ ──── bindService ────────────────→│
│ (MyWidgetService) │
│ │
│ ← ServiceConnection 建立 ────────│
│ │
│ getViewAt(N) │
│ ──── Binder (IRemoteViewsFactory)→│
│ │── 创建 RemoteViews
│ │── 设置文本/图片
│ ← RemoteViews (序列化) ──────────│
│ │
│ rv.apply() → 构建列表项 View │
│ 显示在 ListView 中 │
│ │
│ getViewAt(N+1) │
│ ──── Binder ─────────────────────→│
│ ... │
★ 每个列表项都是独立的跨进程 Binder 调用
★ 所以列表 Widget 性能较差,项数不宜太多
十、完整流程时序图
App 进程 SystemServer Launcher 进程
│ │ │
│ 创建 RemoteViews │ │
│ ┌─────────────────┐ │ │
│ │ pkg: com.ex.app │ │ │
│ │ layoutId: 0x7f0a│ │ │
│ │ actions: [ │ │ │
│ │ setText(...) │ │ │
│ │ setImage(...) │ │ │
│ │ setClick(...) │ │ │
│ │ ] │ │ │
│ └────────┬────────┘ │ │
│ │ │ │
│ updateAppWidget() │ │
│ ──── Binder ───────→│ │
│ (RemoteViews 序列化) │ │
│ writeToParcel() │ │
│ │ │
│ │ AppWidgetServiceImpl │
│ │ 存储 RemoteViews │
│ │ │
│ │ notifyAppWidgetView() │
│ │ ──── Binder ──────────→│
│ │ (RemoteViews 反序列化) │
│ │ │
│ │ │ AppWidgetHostView
│ │ │ .updateAppWidget()
│ │ │
│ │ │ ┌─ apply() ────────┐
│ │ │ │ │
│ │ │ │ ① createPkgCtx │
│ │ │ │ ("com.ex.app") │
│ │ │ │ │
│ │ │ │ ② inflate │
│ │ │ │ (R.layout.xxx) │
│ │ │ │ 用 App 的资源 │
│ │ │ │ │
│ │ │ │ ③ 执行 Actions: │
│ │ │ │ tv.setText("..") │
│ │ │ │ iv.setImageRes() │
│ │ │ │ v.setOnClick(PI) │
│ │ │ │ │
│ │ │ └───────────────────┘
│ │ │
│ │ │ Widget 显示 ✅
│ │ │
│ │ 用户点击 │
│ ← PendingIntent.send() ─────────────────────│
│ 收到回调 │
十一、核心设计思想总结
┌─────────────────────────────────────────────────────────────────┐
│ RemoteViews 设计思想 │
├──────────────────┬──────────────────────────────────────────────┤
│ 命令模式 │ 不传 View 对象,传 Action 操作指令列表 │
│ (Command Pattern)│ 在目标进程中"回放"这些指令构建 View │
├──────────────────┼──────────────────────────────────────────────┤
│ 最小化传输 │ 只传:包名 + 布局ID + Action列表 │
│ │ 几百字节 vs 整个 View 树 │
├──────────────────┼──────────────────────────────────────────────┤
│ 资源延迟加载 │ createPackageContext 在目标进程加载 App 资源 │
│ │ 不需要预先传输所有资源 │
├──────────────────┼──────────────────────────────────────────────┤
│ 安全隔离 │ @RemoteView 白名单 View 类型 │
│ │ @RemotableViewMethod 白名单方法 │
│ │ 防止恶意代码在系统进程执行 │
├──────────────────┼──────────────────────────────────────────────┤
│ PendingIntent │ 点击事件不传 Listener(无法序列化) │
│ 替代 Listener │ 而是传 PendingIntent 跨进程回调 │
├──────────────────┼──────────────────────────────────────────────┤
│ 增量更新 │ apply() 全量构建 / reapply() 只执行 Action │
│ │ Widget 更新时用 reapply 提高效率 │
├──────────────────┼──────────────────────────────────────────────┤
│ Bitmap 优化 │ 小 Bitmap: 直接序列化 │
│ │ 大 Bitmap: Ashmem 共享内存 │
│ │ 资源引用: 只传 resId(最优) │
└──────────────────┴──────────────────────────────────────────────┘