记简单实现动态加载APK
前言
随着时代的进步,技术的发展,不少应用在移动平台上的大小越来越大,比如QQ,从以前的二三十MB到现在的上百MB,简直不可相信,但确实如此,有网友调侃到,以前人问:你能装某大型网游吗?以后人问:你的手机能装QQ吗?让人忍不住发笑。不过,事实上,作为用户,确实不想一个app安装包十分庞大,就只是下载都要一定的时间,给人的体验感极其不好,但是好在安卓系统有插件化的支持。
简单来说,就是将一些核心的代码,打包成app给用户下载,然后其它的一些功能就以插件的形势下载,然后启用。这就极大的优化了用户的体验效果。下面就是这次想要简单分享的动态加载APK文件的主题。
简单的原理
为了更好的称呼,理解,先介绍一下有关本文的一些概念和运行原理吧。
概念
- 宿主app,就是用户需要下载的需要安装的apk文件,用来加载插件的。(本文的宿主app的名称为loadApp)
- 插件app,就是宿主app运行时,从网络或sd卡上加载的一个不用安装就能运行的apk文件。(本文的插件app使用名称是loadedApp)
运行原理
通过宿主app(loadApp)将位于网络或sd卡上的一个插件app(loadedApp)加载并运行的过程。

至于一些细节,后文慢慢研究。
实现
宿主APP
新建一个apk工程,宿主app的名就为loadApp。下面为实现的一些代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| package com.qqq.test.loadapp;
import android.annotation.SuppressLint; import android.app.Activity; import android.content.pm.PackageInfo; import android.os.Environment; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View;
import java.io.File; import java.lang.reflect.Constructor; import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.main_btn).setOnClickListener(this); }
@Override public void onClick(View v) { lanuchTargetActivity(); }
private String mClss; String dexPath = Environment.getExternalStorageDirectory().toString() + File.separator + "loadedApp.apk"; @SuppressLint("NewApi") protected void lanuchTargetActivity() { PackageInfo packageinfo = getPackageManager().getPackageArchiveInfo(dexPath, 1); if ((packageinfo != null)&&(packageinfo.activities.length>0)) { String activityName = packageinfo.activities[0].name; mClss = activityName; launchTargetActivity(mClss); } } @SuppressLint("NewApi") protected void launchTargetActivity(final String className) { File dexOutputDir = this.getDir("loadedDexDir", 0); final String dexOutputPath = dexOutputDir.getAbsolutePath(); ClassLoader classLoader = ClassLoader.getSystemClassLoader(); DexClassLoader dexClassLoader = new DexClassLoader(dexPath,dexOutputPath,null,classLoader); try { Class<?> localClass = dexClassLoader.loadClass(className); Constructor<?> localConstructor = localClass.getConstructor(new Class[]{Activity.class}); Object instense = localConstructor.newInstance(new Object[]{this});
Method onCreate = localClass.getDeclaredMethod("onCreate", new Class[]{Bundle.class}); onCreate.setAccessible(true); Bundle bundle = new Bundle(); onCreate.invoke(instense, new Object[]{bundle}); } catch (Exception e) { e.getStackTrace(); } } }
|
由于需要从sd卡加载loadedApp.apk,就不要忘了在AndroidManifest.xml中配置读写权限。
1 2 3
| <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
插件APP
插件app需要导出到sd卡根目录,方便宿主app加载,当然自定义目录也行。
下面为一些插件app的主要代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| package com.qqq.test.loadedapp;
import android.app.Activity; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.Button; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast;
public class MActivity extends AppCompatActivity {
private Activity mActivity; public MActivity() { super(); } public MActivity(Activity context) { super(); mActivity = context; }
@Override protected void onCreate(Bundle savedInstanceState) { if (mActivity != null) { show("okkkk"); LinearLayout parent = new LinearLayout(mActivity); TextView tv = new TextView(mActivity); tv.setText("new uninstalled loadedApp"); parent.addView(tv); mActivity.setContentView(parent); } else { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mActivity = this; } } public void show(String a) { Toast.makeText(mActivity,a,Toast.LENGTH_SHORT).show(); } }
|
需要注意的是,插件app是以插件的形势运行在宿主app中,所以插件app的一些权限需要在宿主app中配置。其次,资源文件在插件app中不能直接使用,只能使用代码,若要实现,需要添加其它办法。
运行结果
直接安装的插件app:

安装宿主app,使用宿主APP加载的运行结果:

运行时,还要注意,必须开启宿主APP的存储权限,不然无法运行。从结果可以看出,除了标题栏不一样,其它内容还是loadedApp的代码执行的结果。
探究
在loadApp加载插件loadedApp后,在/data/data/宿主app包名/目录下多了一个文件夹,即app_loadedDexDir文件夹,在宿主app代码中有写File dexOutputDir = this.getDir("loadedDexDir", 0);,所以有这么一个文件夹,里面有个loadedApp.dex文件。

我们可以将其提取出来研究
1
| adb pull loadedApp.dex ./
|
然后用JEB打开查看一下里面的内容。

这里就是loadedApp的全部代码,所以,在加密插件方面,这里依然是可以知道插件APP的全部代码,为了安全,可以考虑添加一些检测机制,不然你的代码就很危险了。
其次,这里只是简单的通过例子实现了加载apk,自己可以通过深入研究添加插件app的生命周期控制,以及一些services,资源访问问题等。
参考链接