Android插件化学习之路(七)之DL插件开发该注意的坑

2022-06-22 10:18:03 浏览数 (1)

随着前面几篇博客的学习,相信大家对插件化已经有了比较清楚的认识,然而如何将插件化应用到项目中?网上已经有一些优秀的开源框架,这里要向大家推荐一个开源的动态加载框架DL, 该项目由任玉刚大神发起的,项目地址: https://github.com/singwhatiwanna/dynamic-load-apk,该项目结构图如下:

本篇博客,我主要向大家介绍一下利用DL框架进行开发的具体步骤,还有一些注意事项:

利用DL框架进行开发的步骤

1.首先我们需要从github上获取项目代码: 项目地址: https://github.com/singwhatiwanna/dynamic-load-apk 解压后,项目目录如下

lib目录就是DL的插件库,sample目录是对应的demo

2.引入DL库 宿主工程中只要将dl-lib.jar加入libs即可,然后在gradle中引用

代码语言:javascript复制
compile fileTree(dir: 'libs', include: ['*.jar'])

但是插件中则不同,因为DL插件需要用到DL库的类(),所以需要引入DL库,但是插件是最终要加载到宿主程序中的,宿主程序中也是引入了DL库的,如果常规办法导入DL库,则会有两份DL的拷贝,为了解决这个问题,我们让插件中的DL只是编译的时候用,但是不打包进apk。如何让它参与编译却不被打包进apk呢?在Android-studio中 只需要在插件工程中创建一个目录,比如external-jars,然后把dl-lib.jar和放进去,同时在gradle中追加如下代码即可:

代码语言:javascript复制
provided files('external-jars/dl-lib.jar')

同样的如果宿主程序中用了support-v4.jar,那么插件中原有的support-v4.jar也不能被打包进去,也需要将support-v4.jar放到external-jars同时追加

代码语言:javascript复制
provided files('external-jars/android-support-v4.jar')

3.插件的java代码修改 插件中的所有Activity 必须是继承自DLBasePluginActivity或者是DLBasePluginFragmentActivity。如果原有的为Activity,这里需要改为继承DLBasePluginActivity,如果原来为FragmentActivity,那么需要继承DLBasePluginFragmentActivity。

继承DLBasePluginActivity

代码语言:javascript复制
1.  public class MainActivity extends DLBasePluginActivity

继承DLBasePluginFragmentActivity

代码语言:javascript复制
1.  TestFragmentActivity extends DLBasePluginFragmentActivity

另外原有activity中所有代表context引用的this都必须改写为that 如果要调用另外一个activity,不能使用startActivity(),而是使用startPluginActivity,并且intent也要变为DLIntent:

代码语言:javascript复制
DLIntent intent = new DLIntent(getPackageName(), ListActivity.class);
intent.putExtra(TYPE, item.getNavigationInfo());
startPluginActivity(intent);

4.调用的插件apk 在宿主工程中,首先我们需要获取要调用的插件apk对应的MainActivity,DL的demo中插件路径为 sd卡上的DynamicLoadHost目录,没有的话需要创建,或者根据自己需求进行修改.

代码语言:javascript复制
1.  String pluginFolder = Environment.getExternalStorageDirectory()   "/DynamicLoadHost";  
2.  File file = new File(pluginFolder);  
3.  File[] plugins = file.listFiles();  
4.  if (plugins == null || plugins.length == 0) {  
5.      mNoPluginTextView.setVisibility(View.VISIBLE);  
6.      return;  
7.  }  
8.    
9.  for (File plugin : plugins) {  
10.     PluginItem item = new PluginItem();  
11.     item.pluginPath = plugin.getAbsolutePath();  
12.     item.packageInfo = DLUtils.getPackageInfo(this, item.pluginPath);  
13.     if (item.packageInfo.activities != null && item.packageInfo.activities.length > 0) {  
14.         item.launcherActivityName = item.packageInfo.activities[0].name;  
15.     }  
16.     mPluginItems.add(item);  
17. }  

接着是调起响应的apk,这时需要使用dl-lib.jar: 1) 通过Class.forName的方式获取我们需要调用的插件apk中MainActivity的class对象 2) 就上面提到的,我们需要判断该对象继承自DLBasePluginActivity还是DLBasePluginFragmentActivity,得到对应的代理class对象 3) 使用对应的代理class对象调起插件apk

代码语言:javascript复制
1.  PluginItem item = mPluginItems.get(position);  
2.         Class<?> proxyCls = null;  
3.    
4.         try {  
5.             Class<?> cls = Class.forName(item.launcherActivityName, false,  
6.                     DLClassLoader.getClassLoader(item.pluginPath, getApplicationContext(), getClassLoader()));  
7.             if (cls.asSubclass(DLBasePluginActivity.class) != null) {  
8.                 proxyCls = DLProxyActivity.class;  
9.             }  
10.        } catch (ClassNotFoundException e) {  
11.            e.printStackTrace();  
12.            Toast.makeText(this,  
13.                    "load plugin apk failed, load class "   item.launcherActivityName   " failed.",  
14.                    Toast.LENGTH_SHORT).show();  
15.        } catch (ClassCastException e) {  
16.            // ignored  
17.        } finally {  
18.            if (proxyCls == null) {  
19.                proxyCls = DLProxyFragmentActivity.class;  
20.            }  
21.            Intent intent = new Intent(this, proxyCls);  
22.            intent.putExtra(DLConstants.EXTRA_DEX_PATH,  
23.                    mPluginItems.get(position).pluginPath);  
24.            startActivity(intent);  
25.        }  

DL框架优秀之处

dynamic-load-apk向我们展示了许多优秀的处理方法,比如: 1. 把Activity关键的生命周期方法抽象成DLPlugin接口,ProxyActivity通过DLPlugin代理调用插件Activity的生命周期; 2. 设计一个基础的BasePluginActivity类,插件项目里使用这些基类进行开发,可以以接近常规Android开发的方式开发插件项目; 3. 以类似的方式处理Service的问题; 4. 处理了大量常见的兼容性问题(比如使用Theme资源时出现的问题); 5. 处理了插件项目里的so库的加载问题; 6. 使用PluginPackage管理插件APK,从而可以方便地管理多个插件项目

处理插件项目里的so库的加载 这里需要把插件APK里面的SO库文件解压释放出来,在根据当前设备CPU的型号选择对应的SO库,并使用System.load方法加载到当前内存中来 多插件APK的管理 动态加载一个插件APK需要三个对应的DexClassLoader、AssetManager、Resources实例,可以用组合的方式创建一个PluginPackage类存放这三个变量,再创建一个管理类PluginManager,用成员变量HashMap

DL插件开发注意事项

1.主题 dl的插件必须每个activity都单独设置主题(插件的作者说的是也可以在application上设置主题),但我实际测试,即使application设置了主题也必须每个activity都单独设置主题。 也就是说这样是不行的:

代码语言:javascript复制
1.  <application
2.      android:allowBackup="true"
3.      android:theme="@android:style/Theme.Holo.Light"
4.      android:icon="@drawable/ic_launcher"
5.      android:label="@string/app_name" >
6.      <activity
7.          android:name=".SampleActivity"

9.          android:label="@string/app_name" >
10.         <intent-filter>
11.             <action android:name="android.intent.action.MAIN" />
12.             <category android:name="android.intent.category.LAUNCHER" />
13.         </intent-filter>
14.     </activity>
15. </application>

必须这样:

代码语言:javascript复制
1.  <application
2.      android:allowBackup="true"
3.      android:icon="@drawable/ic_launcher"
4.      android:label="@string/app_name" >
5.      <activity
6.          android:name=".SampleActivity"
7.          android:theme="@android:style/Theme.Holo.Light.DarkActionBar" 
8.          android:label="@string/app_name" >
9.          <intent-filter>
10.             <action android:name="android.intent.action.MAIN" />
11.             <category android:name="android.intent.category.LAUNCHER" />
12.         </intent-filter>
13.     </activity>
14. </application>

注意的是 插件只能用系统主题 不能直接定义主题 不能这样

代码语言:javascript复制
1.  android:theme="@style/AppTheme"

只能这样

代码语言:javascript复制
1.  android:theme="@android:style/Theme.Light"

虽然在某些插件上可能不按照此规则也可以正确运行 ,但是我试过绝大多数多需要满足此条件。

2.插件所需要权限需要在宿主工程中声明 3. 使用DL进行插件apk的开发规范

1) 慎用this(接口除外):因为this指向的是当前对象,即apk中的activity,但是由于activity已经不是常规意义上的activity,所以this是没有意义的,但是如果this表示的是一个接口而不是context,比如activity实现了而一个接口,那么this继续有效。

2) 使用that:既然this不能用,那就用that,that是apk中activity的基类BaseActivity中的一个成员,它在apk安装运行的时候指向this,而在未安装的时候指向宿主程序中的代理activity,anyway,that is better than this。

3) activity的成员方法调用问题:原则来说,需要通过that来调用成员方法,但是由于大部分常用的api已经被重写,所以仅仅是针对部分api才需要通过that去调用用。同时,apk安装以后仍然可以正常运行。

4) 启动新activity的约束:启动外部activity不受限制,启动apk内部的activity有限制,首先由于apk中的activity没注册,所以不支持隐式调用,其次必须通过BaseActivity中定义的新方法startActivityByProxy和startActivityForResultByProxy,还有就是不支持LaunchMode。

5) 目前暂不支持Service、BroadcastReceiver等需要注册才能使用的组件,但广播可以采用代码动态注册

4.插件APK的管理后台 使用动态加载的目的,就是希望可以绕过APK的安装过程升级应用的功能,如果插件APK是打包在主项目内部的那动态加载纯粹是多次一举。更多的时候我们希望可以在线下载插件APK,并且在插件APK有新版本的时候,主项目要从服务器下载最新的插件替换本地已经存在的旧插件。为此,我们应该有一个管理后台,它大概有以下功能:

  1. 上传不同版本的插件APK,并向APP主项目提供插件APK信息查询功能和下载功能;
  2. 管理在线的插件APK,并能向不同版本号的APP主项目提供最合适的插件APK;
  3. 万一最新的插件APK出现紧急BUG,要提供旧版本回滚功能;
  4. 出于安全考虑应该对APP项目的请求信息做一些安全性校验;

5.插件APK合法性校验 加载外部的可执行代码,一个逃不开的问题就是要确保外部代码的安全性,我们可不希望加载一些来历不明的插件APK,因为这些插件有的时候能访问主项目的关键数据。 最简单可靠的做法就是校验插件APK的MD5值,如果插件APK的MD5与我们服务器预置的数值不同,就认为插件被改动过,弃用。

0 人点赞