使用DroidPlugin实践应用的插件化
隨著應用的體積越來越大, 插件化也逐漸受到關注,?參考. 應用插件化把模塊完全解耦, 使用下載更新的方式, 擴展應用, 是平臺化類應用的必然選擇. 國內很多公司實現了各式各樣的方法, 360的DroidPlugin是比較有意思的一個, 使用預占位的方式注冊四大組件, 實現熱更新,?參考, 也可以直接讀源碼理解實現邏輯.
Droid Plugin
Talk is cheap, show you the code! 如何把DroidPlugin用起來呢? 這是我比較關注的事情, 開源的Demo寫的如此悲傷, 我來重新梳理一下, 又添加了幾個功能測試. 引入DroidPlugin作為Submodule的依賴.
Github下載地址?和?測試Apk.
使用方法, 生成測試apk, 和其他若干apk, 放入Download文件夾下.
adb命令:?adb push app-debug.apk /sdcard/Download/app-debug.apk
確保Download文件夾下, 有.apk后綴名的文件.
主要
(1) 插件安裝后, 可以直接啟動, 不需要任何冗余操作.
(2) 宿主的權限要多于插件的權限, 否則會權限不足.
(3) 宿主和插件, 可以通過隱式Intent進行通信.
動畫
1. 主頁
使用TabLayout+ViewPager的架構, 包含兩個頁面, 一個是安裝\刪除頁面, 另一個是啟動\卸載頁面. 為了測試和插件的Intent通信, 增加跳轉功能和顯示信息功能.
/*** 主頁面, 使用TabLayout+ViewPager.* 子頁面, 使用RecyclerView.** @author wangchenlong*/ public class MainActivity extends AppCompatActivity {(R.id.main_tl_tabs) TabLayout mTlTabs; // Tabs(R.id.main_vp_container) ViewPager mVpContainer; // ViewPager(R.id.main_b_goto) Button mBGoto; // 跳轉插件的按鈕protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);ButterKnife.bind(this);PagerAdapter adapter = new PagerAdapter(getSupportFragmentManager());mVpContainer.setAdapter(adapter);mTlTabs.setupWithViewPager(mVpContainer);mBGoto.setOnClickListener(this::gotoPlugin);Intent intent = getIntent();if (intent != null && intent.getStringExtra(PluginConsts.MASTER_EXTRA_STRING) != null) {String words = "say: " + intent.getStringExtra(PluginConsts.MASTER_EXTRA_STRING);Toast.makeText(this, words, Toast.LENGTH_SHORT).show();}}// 跳轉控件private void gotoPlugin(View view) {if (isActionAvailable(view.getContext(), PluginConsts.PLUGIN_ACTION_MAIN)) {Intent intent = new Intent(PluginConsts.PLUGIN_ACTION_MAIN);intent.putExtra(PluginConsts.PLUGIN_EXTRA_STRING, "Hello, My Plugin!");startActivity(intent);} else {Toast.makeText(view.getContext(), "跳轉失敗", Toast.LENGTH_SHORT).show();}}// Action是否允許public static boolean isActionAvailable(Context context, String action) {Intent intent = new Intent(action);return context.getPackageManager().resolveActivity(intent, 0) != null;} }ViewPager適配器
/*** ViewPager的適配器* <p>* Created by wangchenlong on 16/1/8.*/ public class PagerAdapter extends FragmentPagerAdapter {private static final String[] TITLES = {"已安裝","未安裝"};public PagerAdapter(FragmentManager fm) {super(fm);} public Fragment getItem(int position) {if (position == 0) {return new StartFragment(); // 已安裝頁} else {return new StoreFragment(); // 想要安裝頁}} public int getCount() {return TITLES.length;}public CharSequence getPageTitle(int position) {return TITLES[position];} }2. 加載頁
顯示Download文件夾下的Apk信息. 使用RecyclerView實現apk列表, 復用Adapter, 標志位(ApkOperator.TYPE_STORE)區分頁面. 使用Rx異步掃描Download文件夾, 添加至列表. 接收服務連接狀態, 成功則自動顯示Apk.
/*** 安裝Apk的頁面, 使用RecyclerView.* <p>* Created by wangchenlong on 16/1/8.*/ public class StoreFragment extends Fragment {(R.id.list_rv_recycler) RecyclerView mRvRecycler;private ApkListAdapter mStoreAdapter; // 適配器// 服務連接private ServiceConnection mServiceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName name, IBinder service) {loadApks();} public void onServiceDisconnected(ComponentName name) {}}; public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {View view = inflater.inflate(R.layout.fragment_list, container, false);ButterKnife.bind(this, view);return view;} public void onViewCreated(View view, Bundle savedInstanceState) {super.onViewCreated(view, savedInstanceState);LinearLayoutManager llm = new LinearLayoutManager(view.getContext());llm.setOrientation(LinearLayoutManager.VERTICAL);mRvRecycler.setLayoutManager(llm);mStoreAdapter = new ApkListAdapter(getActivity(), ApkOperator.TYPE_STORE);mRvRecycler.setAdapter(mStoreAdapter);if (PluginManager.getInstance().isConnected()) {loadApks();} else {PluginManager.getInstance().addServiceConnection(mServiceConnection);}}// 加載Apkprivate void loadApks() {// 異步加載, 防止Apk過多, 影響速度Observable.just(getApkFromDownload()).subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread()).subscribe(mStoreAdapter::setApkItems);}// 從下載文件夾獲取Apkprivate ArrayList<ApkItem> getApkFromDownload() {File files = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);PackageManager pm = getActivity().getPackageManager();ArrayList<ApkItem> apkItems = new ArrayList<>();for (File file : files.listFiles()) {if (file.exists() && file.getPath().toLowerCase().endsWith(".apk")) {final PackageInfo info = pm.getPackageArchiveInfo(file.getPath(), 0);apkItems.add(new ApkItem(pm, info, file.getPath()));}}return apkItems;}public void onDestroyView() {super.onDestroyView();ButterKnife.unbind(this);PluginManager.getInstance().removeServiceConnection(mServiceConnection);} }適配器, 負責列表顯示, 操作交由ViewHolder進行處理.
/*** 啟動的適配器* <p>* Created by wangchenlong on 16/1/13.*/ public class ApkListAdapter extends RecyclerView.Adapter<ApkItemViewHolder> {private ArrayList<ApkItem> mApkItems;private Activity mActivity;private int mType; // 類型public ApkListAdapter(Activity activity, int type) {mActivity = activity;mApkItems = new ArrayList<>();mType = type;}public void setApkItems(ArrayList<ApkItem> apkItems) {mApkItems = apkItems;notifyDataSetChanged();}public void addApkItem(ApkItem apkItem) {mApkItems.add(apkItem);notifyItemInserted(mApkItems.size() + 1);}public void removeApkItem(ApkItem apkItem) {mApkItems.remove(apkItem);notifyDataSetChanged();}public ApkItem getApkItem(int index) {return mApkItems.get(index);}public int getCount() {return mApkItems.size();} public ApkItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.apk_item, parent, false);return new ApkItemViewHolder(mActivity, view, mType, this::removeApkItem);} public void onBindViewHolder(ApkItemViewHolder holder, int position) {holder.bindTo(mApkItems.get(position));} public int getItemCount() {return mApkItems.size();} }注意, 在設置Item時, 需要刷新列表, 使用notifyDataSetChanged或notifyItemInserted.
ViewHolder, 控制列表點擊事件, 根據頁面類型, 修改調用方法.
/*** Apk的列表, 參考: R.layout.apk_item* <p>* Created by wangchenlong on 16/1/13.*/ public class ApkItemViewHolder extends RecyclerView.ViewHolder {(R.id.apk_item_iv_icon) ImageView mIvIcon; // 圖標(R.id.apk_item_tv_title) TextView mTvTitle; // 標題(R.id.apk_item_tv_version) TextView mTvVersion; // 版本號(R.id.apk_item_b_do) Button mBDo; // 確定按鈕(R.id.apk_item_b_undo) Button mBUndo; // 取消按鈕private ApkItem mApkItem; // Apk項private Context mContext; // 上下文private ApkOperator mApkOperator; // Apk操作private int mType; // 類型/*** 初始化ViewHolder** @param activity Dialog綁定Activity* @param itemView 項視圖* @param type 類型, 加載或啟動* @param callback 刪除Item的調用*/public ApkItemViewHolder(Activity activity, View itemView, int type, ApkOperator.RemoveCallback callback) {super(itemView);ButterKnife.bind(this, itemView);mContext = activity.getApplicationContext();mApkOperator = new ApkOperator(activity, callback); // Apk操作mType = type; // 類型}// 綁定ViewHolderpublic void bindTo(ApkItem apkItem) {mApkItem = apkItem;mIvIcon.setImageDrawable(apkItem.icon);mTvTitle.setText(apkItem.title);mTvVersion.setText(String.format("%s(%s)", apkItem.versionName, apkItem.versionCode));// 修改文字if (mType == ApkOperator.TYPE_STORE) {mBUndo.setText("刪除");mBDo.setText("安裝");} else if (mType == ApkOperator.TYPE_START) {mBUndo.setText("卸載");mBDo.setText("啟動");}mBUndo.setOnClickListener(this::onClickEvent);mBDo.setOnClickListener(this::onClickEvent);}// 點擊事件private void onClickEvent(View view) {if (mType == ApkOperator.TYPE_STORE) {if (view.equals(mBUndo)) {mApkOperator.deleteApk(mApkItem);} else if (view.equals(mBDo)) {// 安裝Apk較慢需要使用異步線程new InstallApkTask().execute();}} else if (mType == ApkOperator.TYPE_START) {if (view.equals(mBUndo)) {mApkOperator.uninstallApk(mApkItem);} else if (view.equals(mBDo)) {mApkOperator.openApk(mApkItem);}}}// 安裝Apk的線程, Rx無法使用.private class InstallApkTask extends AsyncTask<Void, Void, String> {protected void onPostExecute(String v) {Toast.makeText(mContext, v, Toast.LENGTH_LONG).show();}protected String doInBackground(Void... params) {return mApkOperator.installApk(mApkItem);}} }注意, 安裝Apk使用異步線程(AsyncTask), 不能使用Rx.
3. 啟動頁
啟動頁面, 顯示已安裝的Apk, 包含啟動和卸載功能. 與安裝頁不同, 額外增加一個接收器, 負責接收安裝成功之后的廣播, 用于更新列表.
/*** 啟動Apk頁面* <p>* Created by wangchenlong on 16/1/13.*/ public class StartFragment extends Fragment {(R.id.list_rv_recycler) RecyclerView mRvRecycler;private ApkListAdapter mApkListAdapter; // 適配器private InstallApkReceiver mInstallApkReceiver; // Apk安裝接收器// 服務連接private final ServiceConnection mServiceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName name, IBinder service) {loadApks();} public void onServiceDisconnected(ComponentName name) {}}; public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {View view = inflater.inflate(R.layout.fragment_list, container, false);ButterKnife.bind(this, view);return view;} public void onViewCreated(View view, Bundle savedInstanceState) {super.onViewCreated(view, savedInstanceState);LinearLayoutManager llm = new LinearLayoutManager(view.getContext());llm.setOrientation(LinearLayoutManager.VERTICAL);mRvRecycler.setLayoutManager(llm);mApkListAdapter = new ApkListAdapter(getActivity(), ApkOperator.TYPE_START);mRvRecycler.setAdapter(mApkListAdapter);mInstallApkReceiver = new InstallApkReceiver();mInstallApkReceiver.registerReceiver(this.getActivity());if (PluginManager.getInstance().isConnected()) {loadApks();} else {PluginManager.getInstance().addServiceConnection(mServiceConnection);}}public void onDestroyView() {super.onDestroyView();ButterKnife.unbind(this);mInstallApkReceiver.unregisterReceiver(this.getActivity());}// 加載Apkprivate void loadApks() {// 異步加載, 防止Apk過多, 影響速度Observable.just(getApkFromInstall()).subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread()).subscribe(mApkListAdapter::setApkItems);}// 獲取安裝中獲取Apkprivate ArrayList<ApkItem> getApkFromInstall() {ArrayList<ApkItem> apkItems = new ArrayList<>();try {final List<PackageInfo> infos = PluginManager.getInstance().getInstalledPackages(0);if (infos == null) {return apkItems;}final PackageManager pm = getActivity().getPackageManager();// noinspection allfor (final PackageInfo info : infos) {apkItems.add(new ApkItem(pm, info, info.applicationInfo.publicSourceDir));}} catch (RemoteException e) {e.printStackTrace();}return apkItems;}// 安裝Apk接收器private class InstallApkReceiver extends BroadcastReceiver {// 注冊監聽public void registerReceiver(Context context) {IntentFilter filter = new IntentFilter();filter.addAction(PluginManager.ACTION_PACKAGE_ADDED);filter.addAction(PluginManager.ACTION_PACKAGE_REMOVED);filter.addDataScheme("package");context.registerReceiver(this, filter);}// 關閉監聽public void unregisterReceiver(Context context) {context.unregisterReceiver(this);}public void onReceive(Context context, Intent intent) {// 監聽添加和刪除事件if (PluginManager.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {try {PackageManager pm = getActivity().getPackageManager();String pkg = intent.getData().getAuthority();PackageInfo info = PluginManager.getInstance().getPackageInfo(pkg, 0);mApkListAdapter.addApkItem(new ApkItem(pm, info, info.applicationInfo.publicSourceDir));} catch (Exception e) {e.printStackTrace();}} else if (PluginManager.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {String pkg = intent.getData().getAuthority();int num = mApkListAdapter.getCount();ApkItem removedItem = null;for (int i = 0; i < num; i++) {ApkItem item = mApkListAdapter.getApkItem(i);if (TextUtils.equals(item.packageInfo.packageName, pkg)) {removedItem = item;break;}}if (removedItem != null) {mApkListAdapter.removeApkItem(removedItem);}}}} }復用Adapter和ViewHolder, 代碼簡介之道.
4. 方法類
四大方法, 安裝\刪除\啟動\卸載, 在刪除和卸載時, 均會提示Dialog. 注意的是安裝Apk, 耗時較長, 需要使用異步線程.
/*** Apk操作, 包含刪除\安裝\卸載\啟動Apk* <p>* Created by wangchenlong on 16/1/13.*/ public class ApkOperator {public static final int TYPE_STORE = 0; // 存儲Apkpublic static final int TYPE_START = 1; // 啟動Apkprivate Activity mActivity; // 綁定Dialogprivate RemoveCallback mCallback; // 刪除Item的回調public ApkOperator(Activity activity, RemoveCallback callback) {mActivity = activity;mCallback = callback;}// 刪除Apkpublic void deleteApk(final ApkItem item) {AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);builder.setTitle("警告");builder.setMessage("你確定要刪除" + item.title + "么?");builder.setNegativeButton("刪除", (dialog, which) -> {if (new File(item.apkFile).delete()) {mCallback.removeItem(item);Toast.makeText(mActivity, "刪除成功", Toast.LENGTH_SHORT).show();} else {Toast.makeText(mActivity, "刪除失敗", Toast.LENGTH_SHORT).show();}});builder.setNeutralButton("取消", null);builder.show();}/*** 安裝Apk, 耗時較長, 需要使用異步線程** @param item Apk項* @return [0:成功, 1:已安裝, -1:連接失敗, -2:權限不足, -3:安裝失敗]*/public String installApk(final ApkItem item) {if (!PluginManager.getInstance().isConnected()) {return "連接失敗"; // 連接失敗}if (isApkInstall(item)) {return "已安裝"; // 已安裝}try {int result = PluginManager.getInstance().installPackage(item.apkFile, 0);boolean isRequestPermission = (result == PluginManager.INSTALL_FAILED_NO_REQUESTEDPERMISSION);if (isRequestPermission) {return "權限不足";}} catch (RemoteException e) {e.printStackTrace();return "安裝失敗";}return "成功";}// Apk是否安裝private boolean isApkInstall(ApkItem apkItem) {PackageInfo info = null;try {info = PluginManager.getInstance().getPackageInfo(apkItem.packageInfo.packageName, 0);} catch (RemoteException e) {e.printStackTrace();}return info != null;}// 卸載Apkpublic void uninstallApk(final ApkItem item) {AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);builder.setTitle("警告");builder.setMessage("警告,你確定要卸載" + item.title + "么?");builder.setNegativeButton("卸載", (dialog, which) -> {if (!PluginManager.getInstance().isConnected()) {Toast.makeText(mActivity, "服務未連接", Toast.LENGTH_SHORT).show();} else {try {PluginManager.getInstance().deletePackage(item.packageInfo.packageName, 0);mCallback.removeItem(item);Toast.makeText(mActivity, "卸載完成", Toast.LENGTH_SHORT).show();} catch (RemoteException e) {e.printStackTrace();}}});builder.setNeutralButton("取消", null);builder.show();}// 打開Apkpublic void openApk(final ApkItem item) {PackageManager pm = mActivity.getPackageManager();Intent intent = pm.getLaunchIntentForPackage(item.packageInfo.packageName);intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);mActivity.startActivity(intent);}// 刪除Item回調, Adapter調用刪除Itempublic interface RemoveCallback {void removeItem(ApkItem apkItem);} }5. 互動Apk
為了測試DroidPlugin的一些特性, 又寫了一個測試Apk.
測試插件和宿主的通信, 插件類的生命周期.
有時間可以再試試其他公司的插件化. One by One.
OK, that's all! Enjoy It.
原文地址:?http://www.jianshu.com/p/f1217cce93ef
總結
以上是生活随笔為你收集整理的使用DroidPlugin实践应用的插件化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android实战】DroidPlugi
- 下一篇: 探索7.x, 全面解析Activity启