Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

增加 Activity 的 onCreateOptionsMenu 测试用例 #94

Merged
merged 6 commits into from
Aug 17, 2019
Merged

增加 Activity 的 onCreateOptionsMenu 测试用例 #94

merged 6 commits into from
Aug 17, 2019

Conversation

LinXueyuanStdio
Copy link
Contributor

我又来啦
sample-normal-app 里可以打出期待效果,但是在 sample-host 里没有。下面两张截图分别是在 sample-normal-appsample-host 截的。

ShadowActivitymHostActivityDelegator 都没有找到 onCreateOptionsMenu 方法,然后试了下在 ShadowActivity 里加个转调关系如下:

+    public boolean OnCreateOptionsMenu(Menu menu) {
+        return mHostActivityDelegator.superOnCreateOptionsMenu(menu);
+    }

事实证明举一反三失败。呀!这就触及我的知识盲区啦!怎么修复呢?

PS:奇怪的是,其他相关方法比如 OnOptionsItemSelected OnPrepareOptionsMenu 等等在 HostActivityDelegator 里能找到,但是在 ShadowActivity 里找不到,这些方法可能可以连带修复

期待效果 实际效果
Screenshot_2019-08-16-19-19-22-088_com tencent sh Screenshot_2019-08-16-19-18-31-076_com tencent sh

@shifujun
Copy link
Collaborator

这个跟#92 就不一样了。#92 中的API是app代码调用Android系统。而这个问题中的API是Android系统回调app代码。

一般的Android系统回调应该是先回调到PluginContainerActivity上的,可以看到PluginContainerActivity中一堆on开头的方法是怎么实现的。但是这个onCreateOptionsMenu系列方法实际上是android.view.Window#setCallback接口的回调,所以我们有机会让Android系统直接回调PluginActivity。所以在ShadowActivityDelegate中有一行代码:mHostActivityDelegator.window.callback = pluginActivity重新设置了这个callback。PluginActivity中对应方法的实现还是像其他方法一样直接委托回给PluginContainerActivity的super方法,以便app代码中的super方法调用能够正常调用到系统Activity基类上。

下面贴的修复代码中public boolean onCreatePanelMenu(int featureId, Menu menu)方法的实现是抄的系统Activity中的实现,但是你仔细看会发现我没转调Fragment的dispatchCreateOptionsMenu方法。估计Fragment的onCreateOptionsMenu方法还是不能正常工作的,这个希望你试试能不能自己解决。

前面说的,你可能还是一时很难看懂,建议在正常安装的app和插件环境运行的app上,都挂上断点看看调用栈,就能理解了。我修复这个问题时也是这样做的。

这是修复的patch,还是由你合入进来吧。你把你添加的测试用例再改改,去掉跟这个问题没关系的部分。

Index: projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/PluginActivity.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/PluginActivity.java	(date 1565954262000)
+++ projects/sdk/core/runtime/src/main/java/com/tencent/shadow/core/runtime/PluginActivity.java	(date 1565959193000)
@@ -177,11 +177,15 @@
     }
 
     public boolean onCreatePanelMenu(int featureId, Menu menu) {
-        return mHostActivityDelegator.superOnCreatePanelMenu(featureId, menu);
+        if (featureId == Window.FEATURE_OPTIONS_PANEL) {
+            return onCreateOptionsMenu(menu);
+        } else {
+            return mHostActivityDelegator.superOnCreatePanelMenu(featureId, menu);
+        }
     }
 
     public boolean onPreparePanel(int featureId, View view, Menu menu) {
-        return false;
+        return mHostActivityDelegator.superOnPreparePanel(featureId, view, menu);
     }
 
     public void onPanelClosed(int featureId, Menu menu) {
@@ -331,4 +335,8 @@
     public void onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) {
         mHostActivityDelegator.superOnMultiWindowModeChanged(isInMultiWindowMode, newConfig);
     }
+
+    public boolean onCreateOptionsMenu(Menu menu) {
+        return mHostActivityDelegator.superOnCreateOptionsMenu(menu);
+    }
 }

@LinXueyuanStdio
Copy link
Contributor Author

我梳理一下

首先各个core库的继承关系

loader compileOnly runtime, common; api load-parameters
manager compileOnly common; api load-parameters
common, runtime, load-parameters 3个是最基础的库,包含需要共享的信息,比如代理用的接口

它们的职责大概如下

  1. common主要是定义ContentProvider的代理接口,以及定义数据结构给loader和manager共享,如下
    InstalledApk(String apkFilePath, String oDexPath, String libraryPath, byte[] parcelExtras) 
  2. load-parameters只有一个文件,给loader和manager共享
    LoadParameters(String businessName, String partKey, String[] dependsOn, String[] hostWhiteList)
  3. runtime是Shadow的核心,定义了Activity、Fragment、Service等等的代理用的接口,还定义了代理用的转调壳,比如Activity的转调壳PluginContainerActivity。代理的设置通过static字段共享,例如
    public class DelegateProviderHolder {
        public static DelegateProvider delegateProvider;
        ...
    }
  4. loader定义了PluginClassLoader用于加载插件包里的dex,还实现了runtime 里的代理接口,比如 ShadowActivityDelegate 实现了HostActivityDelegate。加载的时候会把这个实现设置到 runtime 包里的 DelegateProviderHolder,然后runtime 从 DelegateProviderHolder 里拿到的,在代码里看是代理接口,实际上就是具体实现,这就让 loader 变得动态化,解耦成功。
  5. manager逻辑简单,啥也不知道,就只会根据LoadParameters,把插件包解压,把相关信息如UUID、插件版本号、插件dex路径等等写入数据库进行管理

Shadow加载用户的插件Activity流程

  1. 用户调用 PluginManager.enter(Context context, long formId, Bundle bundle, EnterCallback callback) 实际上根据formIdbundle构造 InstalledPlugin
  2. 根据 InstalledPlugin,宿主用自己实现的 loader 加载插件。有两个要点
    1. (Shadow SDK)加载时需要初始化 loader 包里的PluginClassLoaderPluginClassLoader 是在 loader 包里被设置到runtime包的static变量里:
      DelegateProviderHolder.setDelegateProvider(mPluginLoader)
      这样之后runtime包获取的代理对象非空,得以实现代理绑定。
      PluginClassLoader里包含了大量具体实现,对应于 runtime 包里的各种代理接口,如
       override fun getHostActivityDelegate(...): HostActivityDelegate {
           return ShadowActivityDelegate(this)
       }
      ShadowActivityDelegate等下讲它做了什么。
    2. (用户)根据用户自定义,最终InstalledPlugin转化为Intent,调用 context.startActivity(intent)
  3. context.startActivity(intent) 最终启动 PluginContainerActivity(这里有些复杂的继承关系,最终是到PluginContainerActivity没错)
  4. PluginContainerActivity 在其构造函数里获取代理并双向绑定
    delegate = DelegateProviderHolder.delegateProvider.getHostActivityDelegate(this.getClass());
    delegate.setDelegator(this); // `this` is PluginContainerActivity
    this.hostActivityDelegate = delegate;
  5. 没啦。系统回调会经过 PluginContainerActivity 传给用户,用户操作会通过 PluginContainerActivity 的代理接口传给系统。这里实际上PluginContainerActivity没有和用户有直接交流,而是和 ShadowActivity 有直接交流。用户的插件Activity已经由原来的android.app.Activity被修改为继承ShadowActivity,通过字节码编辑。

补充一下,ShadowActivityDelegate核心是

val aClass = mPluginClassLoader.loadClass(pluginActivityClassName)
val pluginActivity = PluginActivity::class.java.cast(aClass.newInstance())
initPluginActivity(pluginActivity)
mPluginActivity = pluginActivity
 ...
mHostActivityDelegator.window.callback = pluginActivity

我们知道 class ShadowActivity extends PluginActivity,而用户的Activity被字节码编辑为class UserActivity extends ShadowActivity。所以ShadowActivityDelegate使用mPluginClassLoader加载用户的Activity类,可以转化为抽象的pluginActivity,于是之后绑定系统回调给pluginActivity也等价于绑定给用户的Activity。ShadowActivity本质上不是Android官方的Activity,这里的代理绑定是模拟了官方的Activity。

解决测试用例的问题

因为ShadowActivity是Shadow SDK的自定义类,不是官方Activity,所以用户的插件Activity被字节码编辑成ShadowActivity是有风险的,主要是代理的覆盖面够不够广。对Shadow来说,如果做到 用户用到Android官方的API数 <= Shadow代理的API数 <= Android官方的所有API数就行啦,当然理想状态是 用户用到Android官方的API数 <= Shadow代理的API数 == Android官方的所有API数。这里onCreateOptionsMenu 的问题就是因为 ShadowSDK里的ShadowActivity不够完善。Shadow 官方也说了:

以我们多年的插件环境下业务开发经验,插件框架是不可能一步到位实现完美的。 因此,我们相信大部分业务在接入时都是需要一定的二次开发工作。 Shadow现有的代码满足的是我们自己的业务现在的需求。得益于全动态的设计, 插件框架和插件本身都是动态发布的,插件包里既有插件代码也有插件框架代码, 所以可以根据新版本插件的需要同时开发插件框架。

例如,ShadowActivity没有实现全所有Activity方法,你写的测试代码可能用到了, 就会出现Method Not Found错误,只需要在ShadowActivity中实现对应方法就可以了。 大部分方法的实现都只是需要简单的转调就能工作正常。

最后的解决就是加转调。onCreateOptionsMenu 是系统回调的方法,前面 ShadowActivityDelegate 里设置系统回调代理是给PluginActivity,所以这个转调需要在PluginActivity里加。剩下的就是参考Android官方,把系统回调给代理好。

end

终于理解了,awsl

@shifujun
Copy link
Collaborator

理解的不错。只有common的理解有点偏。

common中其实主要的类是InstalledApk类。ContentProvider相关的类其实是runtime,只是因为由于ContentProvider原理上的限制必须打包在宿主中,所以顺道放在common了。所以它们的包名还是在runtime之下的。

单看core层,common就是loader和manager之间公用的类。加上dynamic层,common就是core层中不能动态化的部分。由于只看core层,core层就都应该是打包在宿主中的,那common改名成host也很奇怪,所以这个名字我也挺纠结的。

相信你基本掌握了扩展Shadow支持系统API的方法,期待你将未来你用的实现也都贡献回Shadow。

@shifujun shifujun merged commit 7d892d7 into Tencent:dev Aug 17, 2019
@shifujun
Copy link
Collaborator

再给一点贡献代码方面的建议。

  1. 提交记录的信息要表明这次提交修改的原因是什么。因为我们回头看提交记录的时候,最重要的是想知道这行代码当初修改是为什么。

  2. 提交的划分,最好将对SDK的修改和Sample的修改分开。由于PR的来源分支是你的私有分支,其实你可以rebase你的PR分支,重新整理提交的。

  3. 如果继续贡献代码的话,建议参考com.tencent.shadow.test.cases.plugin_main.application_info.ApplicationInfoTest直接将测试用例写成自动化测试。并且测试场景尽量不使用XML资源,简化代码。

最后,其实以上建议做不到都没关系,最重要的还是参与进来。

@LinXueyuanStdio
Copy link
Contributor Author

嗯嗯,以后提交PR会多加注意一下,感谢 @shifujun 耐心指导

fengjixuchui added a commit to fengjixuchui/Shadow that referenced this pull request Aug 17, 2019
增加 Activity 的 onCreateOptionsMenu 测试用例 (Tencent#94)
@shifujun shifujun mentioned this pull request Aug 19, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants