編輯:關於Android編程
深受啟發,所以就寫了這遍文章使用文中的方法實現功能的插件化(雖用不同方法來實現的相同功能,但會從(1)中摘錄許多)。
在360安全衛士一些應用中,有些功能需要添加(下載)後才可以運行,例如360安全衛士中的搶紅包功能。
這是因為這些功能被插件化分離出來成一個apk/zip文件,當用戶使用這些功能時,再去下載相應的插件(不安裝插件apk)來實現功能,當然也可以刪除掉插件文件來實現刪除功能的效果,實現了功能模塊的解耦。
【開始時 主應用本身未實現“紅包助手”功能,然後點擊按鈕“添加並運行”按鈕後,下載功能插件(未安裝)後來實現“紅包助手功能”。】
因為要讀文件進行讀寫,在清單文件中進行權限注冊:
MainActivity中“添加並運行”按鈕的點擊事件:加載“搶紅包的功能”
public void loadRedPaper(View view) {
dynamicLoader("redpaper");
}
加載功能插件的函數 dynamicLoader(String pluginName) :
不安裝功能插件apk的情況下,我們接下來要做的就是獲取插件apk中的Activity,使它我們主應用的宿主Activity偷換,使用這個宿主Activity專門來替換功能插件apk的Activity,在插件apk的Activity中實現相關的功能。
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中的Activity的完整類名
* 注意完整類名的設置與功能插件名有關
*/ intent.putExtra("class","com.cxmscb.cxm."+pluginName+".DynamicActivity");
// 啟動宿主Activity:
startActivity(intent);
}
}
查看功能插件apk是否已被下載:
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;
}
二、宿主Activity中的邏輯
宿主Activity專門用來替換功能插件apk/zip中的Activity,但插件Activity銷毀時宿主Activity也會銷毀。
加載外部功能插件apk/zip使用到了DexClassLoader和AssetManager來構建加載插件apk的類加載器和加載插件資源的Resources對象,具體原理可參考 DexClassLoader&AssetManager中的介紹。下面我們直接使用:
public class LoaderActivity extends Activity {
//宿主Activity,專用於加載插件apk的Activity
private String apkPath;//功能插件apk路徑
private String className;//功能插件中Activity的完整類名
//功能插件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 {
// 創建功能插件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中的資源文件進行加載
*/
// 加載插件apk的Activity類
Class activityClass = dexClassLoader.loadClass(className);
// 獲取構造方法並創建Actitiy對象
Constructor localConstructor = activityClass.getConstructor(new Class[]{});
Object instance = localConstructor.newInstance(new Object[]{});
// 調用插件Actitiy中定義的setActivity方法
Method localMethodSetActivity = activityClass.getDeclaredMethod(
"setActivity", new Class[] { Activity.class });
localMethodSetActivity.setAccessible(true);
localMethodSetActivity.invoke(instance, new Object[] { this });
// 再調用插件Activity中的onCreate方法
Method methodOnCreate = activityClass.getDeclaredMethod(
"onCreate", new Class[] { Bundle.class });
methodOnCreate.setAccessible(true);
// 調用時傳入所需的參數:bundle對象
Bundle paramBundle = new Bundle();
paramBundle.putBoolean("KEY_START_FROM_OTHER_ACTIVITY", true);
methodOnCreate.invoke(instance, new Object[] { paramBundle });
} 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中的Activity就可以通過R來訪問資源
}
三、功能插件Apk中的邏輯
功能插件apk中的Activity。
public class DynamicAcitivty extends Activity {
private Activity otherActivity; // 偷換過來的宿主Activity
private View v; // Activity界面View
private Button button; // 界面中的按鈕
private RelativeLayout relativeLayout;
@Override
public void onCreate(Bundle savedInstanceState) {
boolean b = false;
if (savedInstanceState != null) {
b = savedInstanceState.getBoolean("KEY_START_FROM_OTHER_ACTIVITY", false);
if (b) {
// 簡單的解析布局文件,設置Activity界面和按鈕的設置
// 不過注意這裡的上下文Context使用的是偷換過來的Activity
v = LayoutInflater.from(this.otherActivity).inflate(R.layout.fragment_dynamic,null, false);
this.otherActivity.setContentView(v);
button = (Button) v.findViewById(R.id.start);
relativeLayout = (RelativeLayout) v.findViewById(R.id.rl);
button.setOnClickListener(new View.OnClickListener() {
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public void onClick(View v) {
Toast.makeText(otherActivity,"開始搶紅包",Toast.LENGTH_SHORT).show();
// 使用資源設置背景
relativeLayout.setBackground(otherActivity.getResources().getDrawable(R.drawable.zz));
}
});
}
}
if (!b) {
super.onCreate(savedInstanceState);
setContentView(R.layout.fragment_dynamic);
}
}
// 在宿主Activity中偷換Activity的方法:setActivity
public void setActivity(Activity paramActivity) {
this.otherActivity = paramActivity;
}
}
注意功能插件的Activity完整類名的設置,要與主應用的邏輯一致。
後續問題:
1.在插件apk打包後可能會對Activity類名進行混淆,這樣會無法被主應用反射到。
2.上述主應用的邏輯並未完整,為了方便演示省去了皮膚插件的下載(不需要安裝)
3.功能插件apk最好存放在較私密的地方,為了不方便被清理軟件掃描到可更後綴為zip文件
4.既然可以添加插件功能,當然也可以刪除插件功能。再添加一個刪除功能插件apk文件功能即可。
Github : Github
今天我們來看一下一個內存洩漏檢測神器 leakcanary(https://github.com/square/leakcanary)首先我們來看一下leakcanary
完成效果:選擇選項後退出,界面記住選擇選項。1.利用SharePreferences方法記住選擇的選項主布局:
今天遇到一個很奇怪的問題,關於在view裡面更新LRC歌詞的,view裡面有一個成員變量,lrcindex ,在draw裡面會用到它來更新歌詞,歌詞裡面有一行是紅色的,表
前面說到如何使用小米的推送來實現簡單的聊天系統,雖然簡單實現了,但是對於你們來說想要真正的體驗效果就只能跟我互動,為了解決這個缺點,我最近完善了下,增加了添加朋友的功能,