时间:2021-02-15 10:20:34 | 栏目:Android代码 | 点击:次
前言
问题的背景是,视频互动业务需要增加弹幕功能,但是播放器的视图是伪横屏的,即,他是一种类似于使用 rotate(90.0)的方式,旋转横屏的,在 Activity 层面上还是一个竖屏的状态。那么弹幕输入的时候的键盘,也是竖屏的。这会带来比较严重的用户体验问题。
由于屏幕旋转状态在 android 下,是一个 Activity 层面上的事情,而且相当的底层,无从 hook,多方调研以后,决定采拉起一个横屏的 Activity 作为键盘输入的专用 Activity。
这里的代码很快就可以写好,如下所示:
/** * Created by DesGemini on 12/09/2017. */ public class DialogActivity extends Activity { private RelativeLayout mContentView; private View vSendBtn; private EditText etDanmakuInput; private InputMethodManager mInputMethodManager; public static WeakReference<DanmakuWriteCallback> danmakuWriteCallback = new WeakReference<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mInputMethodManager = (InputMethodManager)this.getSystemService(Context.INPUT_METHOD_SERVICE); mContentView = (RelativeLayout) getLayoutInflater() .inflate(R.layout.hiv_danmaku_input_dialog, null); vSendBtn = mContentView.findViewById(R.id.tv_danmaku_send_btn); etDanmakuInput = (EditText) mContentView.findViewById(R.id.et_danmaku_input); vSendBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // 这里需要处理 Activity 间回传逻辑 } }); setContentView(mContentView); showSoftKeyboard(); } private boolean showSoftKeyboard() { if (this.etDanmakuInput == null) { return false; } else { etDanmakuInput.postDelayed(new Runnable() { public void run() { etDanmakuInput.requestFocus(); mInputMethodManager.showSoftInput(etDanmakuInput, 0); } }, 100L); return true; } } @Override protected void onPause() { super.onDestroy(); danmakuWriteCallback.getAndSet(null); } @Override public void finish() { super.finish(); } }
DTO 的代码定义如下所示:
public class DanmakuDialogDTO implements Serializable { public WeakReference<DWDanmakuWriteController.DanmakuWriteCallback> callback; public Map<String, String> utExtraParams; }
那么现在问题来了,怎么把这个 Activity 获取到的 String 带回去?
最自然的想法是 onActivityResult,然而,播放器是一个 sdk,写不了 Activity 里的代码,也不可能通知许多业务方一一做改动。
那就只能抛开 android 原生的 Activity 间拉起结束中的通信机制了,思考其他可以通信的方法。很自然地,我们想到了 Callback 。结构如下图。但是 Callback 这样的一个非基本数据类型的对象怎么在 Activity 间传递呢?
尝试通过存入 Intent 的 Extras的方式,然而 putExtra 方法并不能 put 一个 object,只能 put 一个 serializable。那就让这个 DTO(Data Transfer Object)implements serializable 接口吧。没有问题。
然而无法启动 Activity,会有一个 crash 抛出:
java.lang.NullPointerException: Expected to unbox a 'int' primitive type but was returned null
报错堆栈如下:
$Proxy1.startActivity(Unknown Source) android.app.Instrumentation.execStartActivity(Instrumentation.java:1520) android.taobao.atlas.runtime.InstrumentationHook$2$1.execStartActivity(InstrumentationHook.java:299)
如果把这个 DTO 的成员变量改为 static 类型,则可以启动 Activity。
背后的原因是因为,在常规的序列化过程中,浅拷贝其实是没什么意义的。浅拷贝意味着复制一个引用的地址,是一个内存地址,但是常规序列化,要么跨进程,要么就是网络传输,序列化为 JSON,在这些常规场景里内存地址没有意义。因此 Java 序列化没有浅拷贝的选项,也往往是针对一个 POJO 或者 Bean 进行序列化,而不会对一个一般的含有很多引用的类进行序列化。
然而 Android 中的 Activity 与 Activity 间的传递对象又有所不同,理论上,都在同一个 Dalvik VM 中运行,相互的类引用都是可以访问到的。但是由于 Android Intent 设计为序列化传递,序列化过程中没有设计浅拷贝的机制,因此就无法浅拷贝地传递引用过去。
那么为什么设为 static 以后就可以传递,不会导致 crash 了呢?是因为静态成员属于类级别的,虽然不能序列化,但是因为我是在同一个机器(而且是同一个进程),我的jvm已经把这个类连带着他的静态变量一起加载进来了,所以获取到的是类层面上的静态变量地址,故,功能正常。
那么就决定是使用public static WeakReference<DWDanmakuWriteController.DanmakuWriteCallback> callback;
了。但是事实上遇到了另一个问题:
在第一次 startActivity 的时候,观察到 Android 做了一次 GC,然后该 WeakReference 就被释放了,因此 Callback 的业务功能也不能正常执行。引入 WeakReference,原本是为了避开 static cakllback 导致的可能的内存泄漏,然而在这种主动 GC 的情况下,WeakReference 失效了。如果改用 SoftReference,和强引用并没有什么区别,都不能避免内存的泄漏。
最终,采用 AtomReference 来持有这个 static callback,在 Activity 退出的时机去将 AtomicReference 置空。之所以使用 AtomicReference,是因为考虑到视频 sdk 有并发场景的可能性,避免一边置 null 另一边准备使用的可能。
总结