安卓之插件化开发使用 DexClassLoader&AssetManager 实现功能插件化

MayFrayne 8年前
   <ol>     <li> <p>在360安全卫士一些应用中,有些功能需要添加(下载)后才可以运行,例如360安全卫士中的抢红包功能。</p> </li>     <li> <p>这是因为这些功能被插件化分离出来成一个apk/zip文件,当用户使用这些功能时,再去下载相应的插件(不安装插件apk)来实现功能,当然也可以删除掉插件文件来实现删除功能的效果,实现了功能模块的解耦。</p> </li>    </ol>    <h3>Demo项目的效果图:</h3>    <p><img src="https://simg.open-open.com/show/ceab385c8c819f7efffd96e074532b3f.gif"></p>    <p>【开始时 主应用本身未实现“红包助手”功能,然后点击按钮“添加并运行”按钮后,下载功能插件(未安装)后来实现“红包助手功能”。】</p>    <h2>一、主应用apk中的逻辑</h2>    <ol>     <li> <p>因为要读文件进行读写,在清单文件中进行权限注册:</p> <pre>  <code class="language-java"><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /></code></pre> </li>     <li> <p>MainActivity中“添加并运行”按钮的点击事件:加载“抢红包的功能”</p> <pre>  <code class="language-java">public void loadRedPaper(View view) {        dynamicLoader("redpaper");     }</code></pre> </li>     <li> <p>加载功能插件的函数 <strong>dynamicLoader(String pluginName)</strong> :</p> <p>不安装功能插件apk的情况下,启动 <strong>插件apk中的Activity</strong> 的方案一般是不可以的,因为插件中的Activity没有在我们的主应用的 <strong>清单文件mainfests</strong> 中注册过,又因为 <strong>Fragment</strong> 不需要注册。所以我们接下来要做的就是获取插件apk中的Fragment,使它加载在我们主应用的 <strong>宿主Activity</strong> 中,使用这个宿主Activity专门来装载功能插件apk的Fragment,在Fragment中实现相应的功能。</p> <pre>  <code class="language-java">private void dynamicLoader(String pluginName) {        // 查找功能插件apk是否存在:        String apkPath = findPlugin(pluginName);      if(apkPath==null){            // 不存在时可以从网络上下载,为方便演示这里先忽略          Toast.makeText(this,"请先下载该插件apk",Toast.LENGTH_SHORT).show();        }else {            // 启动装载Fragment的宿主Activity          Intent intent = new Intent(this,LoaderActivity.class);            //传递功能插件apk的存放路径          intent.putExtra("apkPath",apkPath);            /** 传递功能插件apk中的功能Fragment的完整类名           * 注意完整类名的设置与功能插件名有关           */                                 intent.putExtra("class","com.cxmscb.cxm."+pluginName+".DynamicFragment");            // 启动宿主Activity:          startActivity(intent);        }    }</code></pre> </li>     <li> <p>查看功能插件apk是否已被下载:</p> <pre>  <code class="language-java">private String findPlugin(String pluginName) {        //为方便演示,这里直接将插件apk放置在SD卡根目录       String apkPath = Environment.getExternalStorageDirectory().getAbsolutePath()+ File.separator+pluginName+".apk";        File apk = new File(apkPath);        if(apk.exists()){          return apkPath;      }        return null;    }</code></pre> </li>    </ol>    <h2>二、宿主Activity中的逻辑</h2>    <p>宿主Activity专门用来加载功能插件apk/zip中的Fragment。</p>    <p>加载外部功能插件apk/zip使用到了DexClassLoader和AssetManager来构建加载插件apk的类加载器和加载插件资源的Resources对象,具体原理可参考 <a href="/misc/goto?guid=4959740244469779428" rel="nofollow,noindex">DexClassLoader&AssetManager</a> 中的介绍。</p>    <p>为加载插件apk中的类,我们需要构造一个自己的DexClassLoader来加载插件apk中的dex文件,这样插件中的类才能被找到。</p>    <p>下面我们直接使用:</p>    <pre>  <code class="language-java">public class LoaderActivity extends Activity {          //宿主Activity,专用于加载插件apk的Fragment        private String apkPath;//功能插件apk路径      private String className;//功能插件中Fragment的完整类名        //功能插件apk的类加载器、资源对象、资源管理器      private DexClassLoader dexClassLoader;      private Resources resources;      private AssetManager assetManager;            @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);            Intent intent = getIntent();          apkPath = intent.getStringExtra("apkPath");          className = intent.getStringExtra("class");            try {                // 先准备好装载插件Fragment的容器布局:              FrameLayout frameLayout = new FrameLayout(this);              frameLayout.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));                // 设置布局id,以备Fragment的插入              frameLayout.setId(2);                //设置宿主Activity界面              setContentView(frameLayout);                // 创建功能插件apk的类加载器              dexClassLoader = new DexClassLoader(apkPath,this.getDir("dex",Context.MODE_PRIVATE).getAbsolutePath(),null,super.getClassLoader());                // 创建功能插件apk的资源管理器              assetManager = AssetManager.class.newInstance();              AssetManager.class.getDeclaredMethod("addAssetPath", String.class)                      .invoke(assetManager, apkPath);                // 创建功能插件apk的资源对象              resources = new Resources(assetManager,this.getResources().getDisplayMetrics(),this.getResources().getConfiguration());                /** 创建好上面三个对象后,重写宿主Activity的三个方法:               *  getClassLoader()、getResources()、getAssetManager()               *  这样就可以使用了这三个对象来对功能插件apk中的Fragment进行加载               */                   // 通过反射获取Fragment对象              Fragment fragment = (Fragment) dexClassLoader.loadClass(className).newInstance();                FragmentManager fm = getFragmentManager();              FragmentTransaction fragmentTransaction = fm.beginTransaction();                // 将Fragment对象放入前面定义好的布局当中              fragmentTransaction.add(2,fragment);              fragmentTransaction.commit();              } catch (InstantiationException e) {              e.printStackTrace();          } catch (IllegalAccessException e) {              e.printStackTrace();          } catch (NoSuchMethodException e) {              e.printStackTrace();          } catch (InvocationTargetException e) {              e.printStackTrace();          } /*catch (ClassNotFoundException e) {              e.printStackTrace();          }*/ catch (ClassNotFoundException e) {              e.printStackTrace();          }          }          @Override      public ClassLoader getClassLoader() {          return dexClassLoader==null?super.getClassLoader():dexClassLoader;      }        @Override      public Resources getResources() {          return resources==null?super.getResources():resources;      }          public AssetManager getAssetManager() {          return assetManager==null?super.getAssets():assetManager;      }        //这样一来,在apk中的Fragment就可以通过R来访问资源    }</code></pre>    <h2>三、功能插件Apk中的逻辑</h2>    <ol>     <li> <p>创建功能插件的Fagment及其布局文件。</p> <pre>  <code class="language-java">public class DynamicFragment extends Fragment {          @Override      public View onCreateView(LayoutInflater inflater, final ViewGroup container,                               Bundle savedInstanceState) {            //简单地解析layout文件、获取控件和设置监听            View v = inflater.inflate(R.layout.fragment_dynamic, container, false);            final Button button = (Button) v.findViewById(R.id.start);            button.setOnClickListener(new View.OnClickListener() {              @Override              public void onClick(View v) {                    // 弹出Toast,注意Context的传参。                  Toast.makeText(getActivity().getApplication(),"开始抢红包",Toast.LENGTH_SHORT).show();              }          });            return v;      }    }</code></pre> </li>     <li> <p>注意功能插件的Fragment完整类名的设置,要与主应用的逻辑一致。例:</p> <p><img src="https://simg.open-open.com/show/588c5f33552b76ed0686964b1cc37c64.jpg"></p> </li>     <li> <p>皮肤插件不需要启动Activity:可以清除Activity及其布局文件及其注册。</p> </li>    </ol>    <h3>后续问题:</h3>    <p>1.在插件apk打包后可能会对Fragment类名进行混淆,这样会无法被主应用反射到。</p>    <p>2.上述主应用的逻辑并未完整,为了方便演示省去了皮肤插件的下载(不需要安装)</p>    <p>3.功能插件apk最好存放在较私密的地方,为了不方便被清理软件扫描到可更后缀为zip文件</p>    <p>4.既然可以添加插件功能,当然也可以删除插件功能。再添加一个删除功能插件apk文件功能即可。</p>    <h3> </h3>    <p> </p>