说说RemoteViews

3 阅读11分钟

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          // App 包名(用于加载资源)
├── int mLayoutId            // 布局资源 ID
├── List<Action> mActions    // ★ 操作指令列表(核心)
│
├── setTextViewText()        → 添加 ReflectionAction
├── setImageViewResource()   → 添加 ReflectionAction
├── setImageViewBitmap()     → 添加 BitmapReflectionAction
├── setViewVisibility()      → 添加 ReflectionAction
├── setOnClickPendingIntent()→ 添加 SetOnClickPendingIntent
├── setInt()                 → 添加 ReflectionAction
├── setBundle()              → 添加 ReflectionAction
├── ...
│
├── writeToParcel()          // 序列化: 包名 + 布局ID + Action列表
├── apply()                  // 在目标进程: inflate + 执行所有 Action
└── reapply()                // 增量更新: 只执行 Action,不重新 inflate

2.2 Action 基类

// frameworks/base/core/java/android/widget/RemoteViews.java

public class RemoteViews implements Parcelable {

    private String mPackage;           // 来源 App 的包名
    private int mLayoutId;             // 布局资源 ID
    private ArrayList<Action> mActions; // 操作指令列表

    // ★ Action 抽象基类
    private abstract static class Action implements Parcelable {
        int viewId;  // 目标 View 的 ID

        // 在目标进程中执行此操作
        public abstract void apply(View root, ViewGroup rootParent,
                ActionApplyParams params) throws ActionException;

        // 序列化标识
        public abstract int getActionTag();
    }
}

2.3 核心 Action 子类

// ━━━━━━━━━━ ① ReflectionAction(通用反射操作)━━━━━━━━━━
// 通过反射调用 View 的任意方法

private static class ReflectionAction extends Action {
    String methodName;    // 方法名: "setText", "setVisibility" ...
    int type;             // 参数类型
    Object value;         // 参数值

    @Override
    public void apply(View root, ViewGroup rootParent,
            ActionApplyParams params) {
        // ① 找到目标 View
        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());     // Action 类型标识
        dest.writeInt(viewId);              // 目标 View ID
        dest.writeString(methodName);       // 方法名
        dest.writeInt(type);                // 参数类型
        writeValue(dest, type, value);      // 参数值
    }
}

// 使用示例:
// rv.setTextViewText(R.id.title, "Hello")
// → 内部创建 ReflectionAction:
//   viewId = R.id.title
//   methodName = "setText"
//   type = STRING
//   value = "Hello"
// → apply 时等价于:
//   root.findViewById(R.id.title).setText("Hello")


// ━━━━━━━━━━ ② BitmapReflectionAction(Bitmap 操作)━━━━━━━━━━
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 通过 Ashmem 传递(大图不受 Binder 1MB 限制)
        bitmap.writeToParcel(dest, flags);
    }
}

// 使用示例:
// rv.setImageViewBitmap(R.id.icon, bitmap)
// → apply 时等价于:
//   root.findViewById(R.id.icon).setImageBitmap(bitmap)


// ━━━━━━━━━━ ③ SetOnClickPendingIntent(点击事件)━━━━━━━━━━
private static class SetOnClickPendingIntent extends Action {
    PendingIntent pendingIntent;

    @Override
    public void apply(View root, ViewGroup rootParent,
            ActionApplyParams params) {
        View target = root.findViewById(viewId);
        
        // ★ 不是传 OnClickListener(无法跨进程)
        // 而是设置 PendingIntent,点击时触发
        target.setOnClickListener(v -> {
            try {
                pendingIntent.send();
                // → 跨进程回调到 App 端
            } catch (PendingIntent.CanceledException e) {
                // PendingIntent 已取消
            }
        });
    }
}


// ━━━━━━━━━━ ④ SetRemoteViewsAdapterIntent(列表适配器)━━━━━━━━━━
private static class SetRemoteViewsAdapterIntent extends Action {
    Intent intent;  // 指向 RemoteViewsService

    @Override
    public void apply(View root, ViewGroup rootParent,
            ActionApplyParams params) {
        // 为 ListView/StackView 等设置远程适配器
        AdapterView<?> target = (AdapterView<?>) root.findViewById(viewId);
        
        // 创建远程适配器
        // 通过 ServiceConnection 绑定到 App 进程的 RemoteViewsService
        // 逐项获取 RemoteViews
        target.setRemoteViewsAdapter(intent);
    }
}


// ━━━━━━━━━━ ⑤ ViewGroupActionAdd/Remove(动态添加/移除 View)━━━━━━━━━━
private static class ViewGroupActionAdd extends Action {
    RemoteViews mNestedViews;  // 嵌套的 RemoteViews

    @Override
    public void apply(View root, ...) {
        ViewGroup target = (ViewGroup) root.findViewById(viewId);
        // 递归 apply 嵌套的 RemoteViews
        View nestedView = mNestedViews.apply(context, target);
        target.addView(nestedView);
    }
}

三、RemoteViews 的 API 与对应 Action

// ━━━━━━ 常用 API 与内部 Action 映射 ━━━━━━

public class RemoteViews implements Parcelable {

    // ──── 文本操作 ────
    public void setTextViewText(int viewId, CharSequence text) {
        // → ReflectionAction("setText", text)
        addAction(new ReflectionAction(viewId, "setText",
                ReflectionAction.CHAR_SEQUENCE, text));
    }

    public void setTextColor(int viewId, int color) {
        // → ReflectionAction("setTextColor", 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) {
        // → ReflectionAction("setImageResource", srcId)
        // ★ 只传资源 ID,不传 Bitmap
        addAction(new ReflectionAction(viewId, "setImageResource",
                ReflectionAction.INT, srcId));
    }

    public void setImageViewBitmap(int viewId, Bitmap bitmap) {
        // → BitmapReflectionAction("setImageBitmap", bitmap)
        // ★ Bitmap 通过 Ashmem 传递
        addAction(new BitmapReflectionAction(viewId,
                "setImageBitmap", bitmap));
    }

    public void setImageViewIcon(int viewId, Icon icon) {
        // → ReflectionAction 传递 Icon 对象
        addAction(new ReflectionAction(viewId, "setImageIcon",
                ReflectionAction.ICON, icon));
    }

    public void setImageViewUri(int viewId, Uri uri) {
        // → ReflectionAction("setImageURI", uri)
        addAction(new ReflectionAction(viewId, "setImageURI",
                ReflectionAction.URI, uri));
    }

    // ──── 可见性操作 ────
    public void setViewVisibility(int viewId, int visibility) {
        // → ReflectionAction("setVisibility", 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) {
        // ★ 不能传 OnClickListener(不可序列化)
        // 使用 PendingIntent 代替
        addAction(new SetOnClickPendingIntent(viewId, pi));
    }

    public void setOnClickFillInIntent(int viewId, Intent fillInIntent) {
        // 用于列表项的点击(配合 setPendingIntentTemplate)
        addAction(new SetOnClickFillInIntent(viewId, fillInIntent));
    }

    public void setPendingIntentTemplate(int viewId, PendingIntent pi) {
        addAction(new SetPendingIntentTemplate(viewId, pi));
    }

    // ──── ProgressBar ────
    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));
    }

    // ──── 布局内添加/移除 View ────
    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

// RemoteViews.java

@Override
public void writeToParcel(Parcel dest, int flags) {
    // ① 写入标识信息
    dest.writeInt(MODE_NORMAL);

    // ② 写入应用信息(用于加载资源)
    mApplication.writeToParcel(dest, flags);
    // 包含:packageName, uid

    // ③ 写入布局 ID
    dest.writeInt(mLayoutId);
    // 如: R.layout.widget_layout = 0x7f0a0001

    // ④ 写入 Action 列表
    if (mActions != null) {
        int count = mActions.size();
        dest.writeInt(count);

        for (int i = 0; i < count; i++) {
            Action a = mActions.get(i);
            // 先写 Action 类型标识
            dest.writeInt(a.getActionTag());
            // 再写 Action 内容
            a.writeToParcel(dest, flags);
        }
    } else {
        dest.writeInt(0);
    }
}

4.2 反序列化 (createFromParcel)

// RemoteViews.java

public RemoteViews(Parcel parcel) {
    // ① 读取应用信息
    mApplication = ApplicationInfo.CREATOR.createFromParcel(parcel);

    // ② 读取布局 ID
    mLayoutId = parcel.readInt();

    // ③ 读取 Action 列表
    int count = parcel.readInt();
    mActions = new ArrayList<>(count);

    for (int i = 0; i < count; i++) {
        // 读取 Action 类型标识
        int tag = parcel.readInt();

        // ★ 根据 tag 创建对应的 Action 子类
        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);
        // ... 更多 Action 类型
        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() 完整流程

// RemoteViews.java

/**
 * 在目标进程中,根据 RemoteViews 构建真实的 View
 * @param context  目标进程的 Context
 * @param parent   父 ViewGroup
 * @return 构建好的 View 树
 */
public View apply(Context context, ViewGroup parent) {
    return apply(context, parent, null /* handler */);
}

public View apply(Context context, ViewGroup parent,
        OnClickHandler handler) {

    // ━━━ Step 1: 创建 App 端的 Context ━━━
    // ★ 关键!需要能加载 App 的资源(布局XML、drawable等)
    Context remoteContext = getContextForResources(context);

    // ━━━ Step 2: inflate 布局 ━━━
    // 使用 App 的 Context 来 inflate,这样能加载 App 的 XML 布局
    LayoutInflater inflater = LayoutInflater.from(context);
    inflater = inflater.cloneInContext(remoteContext);

    // ★ 用 App 包名对应的 Resources 来 inflate
    View result = inflater.inflate(mLayoutId, parent, false);

    // ━━━ Step 3: 执行所有 Action ━━━
    performApply(result, parent, params);

    return result;
}

private void performApply(View v, ViewGroup parent,
        ActionApplyParams params) {
    if (mActions != null) {
        for (Action a : mActions) {
            // ★ 逐个执行 Action
            a.apply(v, parent, params);
        }
    }
}

/**
 * 创建能加载 App 资源的 Context
 */
private Context getContextForResources(Context context) {
    try {
        // ★ 通过 App 的包名创建一个能访问 App 资源的 Context
        return context.createPackageContext(
                mApplication.packageName,
                Context.CONTEXT_RESTRICTED);
        // CONTEXT_RESTRICTED: 安全限制,防止恶意操作
    } catch (PackageManager.NameNotFoundException e) {
        return context;
    }
}

5.2 reapply() —— 增量更新

/**
 * 不重新 inflate,只在已有 View 上重新执行 Action
 * 用于 Widget 更新时提高效率
 */
public void reapply(Context context, View v) {
    // 不需要重新 inflate 布局
    // 直接在现有 View 上执行 Action 列表
    performApply(v, (ViewGroup) v.getParent(), params);
}

// apply() vs reapply():
//
// apply():   inflate 布局 + 执行 Action → 返回新 View
// reapply(): 只执行 Action → 在原 View 上更新
//
// Widget 首次显示: apply()
// Widget 更新:     reapply()(更高效)

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);   │
│  │     │      // inflate + 执行 Action                        │
│  │     │      hostView.addView(v);                           │
│  │     │  } else {                                           │
│  │     │      remoteViews.reapply(context, existingView);    │
│  │     │      // 只执行 Action(增量更新)                     │
│  │     │  }                                                  │
│  │     │                                                     │
│  │     │  ⑥ 用户点击按钮                                      │
│  │     │  → PendingIntent.send()                             │
│  │     │  → 跨进程回调到 App                                  │
│  │     │                                                     │
└──────────────────────────────────────────────────────────────┘

6.2 AppWidgetHostView

// frameworks/base/core/java/android/appwidget/AppWidgetHostView.java

public class AppWidgetHostView extends FrameLayout {

    private RemoteViews mRemoteViews;  // 当前的 RemoteViews
    private View mView;                // 当前显示的 View

    /**
     * 更新 Widget 内容
     */
    public void updateAppWidget(RemoteViews remoteViews) {
        mRemoteViews = remoteViews;

        if (remoteViews == null) {
            // 显示默认视图
            showDefaultView();
            return;
        }

        // ★ 判断是否可以增量更新
        boolean recycled = false;
        if (mView != null && canRecycleView(mView)) {
            try {
                // 增量更新:reapply
                remoteViews.reapply(mContext, mView);
                recycled = true;
            } catch (RuntimeException e) {
                // reapply 失败,降级为全量更新
            }
        }

        if (!recycled) {
            // 全量构建:apply
            try {
                mView = remoteViews.apply(mContext, this);
            } catch (RuntimeException e) {
                showErrorView();
                return;
            }

            // 替换当前 View
            removeAllViews();
            addView(mView);
        }
    }

    /**
     * 判断是否可以复用现有 View(布局 ID 相同)
     */
    private boolean canRecycleView(View v) {
        // 如果布局 ID 相同,可以 reapply
        return mRemoteViews != null
                && mRemoteViews.getLayoutId() == mLastLayoutId;
    }
}

七、在 Notification 中的应用

7.1 通知使用 RemoteViews

// Notification 内部使用 RemoteViews 来构建通知视图

// === 标准通知(系统自动生成 RemoteViews)===
Notification notification = new Notification.Builder(context, channelId)
        .setSmallIcon(R.drawable.ic_notif)
        .setContentTitle("Title")
        .setContentText("Content")
        .build();

// Notification.Builder.build() 内部:
// → createContentView()
//   → 创建 RemoteViews(R.layout.notification_template_material_base)
//   → remoteViews.setTextViewText(R.id.title, mContentTitle)
//   → remoteViews.setTextViewText(R.id.text, mContentText)
//   → remoteViews.setImageViewIcon(R.id.icon, mSmallIcon)


// === 自定义通知布局 ===
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 布局限制

// RemoteViews 只支持有限的 View 类型!

// ✅ 支持的 Layout:
FrameLayout
LinearLayout
RelativeLayout
GridLayout

// ✅ 支持的 View:
TextView
ImageView
Button
ImageButton
ProgressBar
Chronometer
AnalogClock
ViewFlipper
ListView        // 需要 RemoteViewsService
GridView        // 需要 RemoteViewsService
StackView       // 需要 RemoteViewsService
AdapterViewFlipper
ViewStub

// ❌ 不支持:
EditText         // 安全风险
WebView          // 安全风险
SurfaceView      // 无法跨进程渲染
RecyclerView     // 不支持
自定义 View       // 不支持

// 原因:安全性 + 兼容性
// RemoteViews 在其他进程中 inflate
// 如果允许自定义 View,恶意 App 可以在 SystemUI/Launcher 中执行任意代码
// frameworks/base/core/java/android/widget/RemoteViews.java

// 安全检查:inflate 时验证 View 类型
private View inflateView(Context context, RemoteViews rv,
        ViewGroup parent) {

    // ... inflate 后检查 ...

    // ★ 验证 View 是否带有 @RemoteView 注解
    if (!view.getClass().isAnnotationPresent(RemoteView.class)) {
        throw new ActionException("Cannot use " +
                view.getClass().getName() + " in RemoteViews");
    }
}

// 在系统 View 源码中:
@RemoteView  // ★ 标注允许在 RemoteViews 中使用
public class TextView extends View { ... }

@RemoteView
public class ImageView extends View { ... }

@RemoteView
public class ProgressBar extends View { ... }

// 没有 @RemoteView 的 View 无法在 RemoteViews 中使用

8.2 反射安全限制

// ReflectionAction.apply() 中的安全检查

@Override
public void apply(View root, ViewGroup rootParent, ...) {
    View target = root.findViewById(viewId);

    Method method = target.getClass().getMethod(methodName, paramType);

    // ★ 检查方法是否带有 @android.view.RemotableViewMethod 注解
    if (!method.isAnnotationPresent(RemotableViewMethod.class)) {
        throw new ActionException("Cannot call method " +
                methodName + " - not annotated with @RemotableViewMethod");
    }

    method.invoke(target, value);
}

// 在 View 源码中:
public class TextView extends View {

    @RemotableViewMethod  // ★ 允许通过 RemoteViews 调用
    public void setText(CharSequence text) { ... }

    @RemotableViewMethod
    public void setTextColor(int color) { ... }

    // 没有 @RemotableViewMethod 的方法不能通过 RemoteViews 调用
    // 如 setOnClickListener() 就没有此注解
}

九、列表 Widget 的特殊处理

// 带列表的 Widget(ListView/GridView/StackView)
// 需要使用 RemoteViewsService + RemoteViewsFactory

// ━━━ App 端 ━━━

// 1. RemoteViewsService
public class MyWidgetService extends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new MyRemoteViewsFactory(this, intent);
    }
}

// 2. RemoteViewsFactory(类似 Adapter)
public class MyRemoteViewsFactory implements RemoteViewsFactory {

    private List<String> mItems;

    @Override
    public void onCreate() {
        mItems = loadData();
    }

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

    // ★ 为每一项返回一个 RemoteViews
    @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() { }
}

// 3. AppWidgetProvider 中设置适配器
@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(最优)                   │
└──────────────────┴──────────────────────────────────────────────┘