• 记简单实现动态加载APK

    前言

    随着时代的进步,技术的发展,不少应用在移动平台上的大小越来越大,比如QQ,从以前的二三十MB到现在的上百MB,简直不可相信,但确实如此,有网友调侃到,以前人问:你能装某大型网游吗?以后人问:你的手机能装QQ吗?让人忍不住发笑。不过,事实上,作为用户,确实不想一个app安装包十分庞大,就只是下载都要一定的时间,给人的体验感极其不好,但是好在安卓系统有插件化的支持。

    简单来说,就是将一些核心的代码,打包成app给用户下载,然后其它的一些功能就以插件的形势下载,然后启用。这就极大的优化了用户的体验效果。下面就是这次想要简单分享的动态加载APK文件的主题。

    简单的原理

    为了更好的称呼,理解,先介绍一下有关本文的一些概念和运行原理吧。

    概念

    • 宿主app,就是用户需要下载的需要安装的apk文件,用来加载插件的。(本文的宿主app的名称为loadApp)
    • 插件app,就是宿主app运行时,从网络或sd卡上加载的一个不用安装就能运行的apk文件。(本文的插件app使用名称是loadedApp)

    运行原理

    通过宿主app(loadApp)将位于网络或sd卡上的一个插件app(loadedApp)加载并运行的过程。

    image-20220630191152016

    至于一些细节,后文慢慢研究。

    实现

    宿主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);//实例化activity
    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加载的运行结果:

    动态加载apk运行

    运行时,还要注意,必须开启宿主APP的存储权限,不然无法运行。从结果可以看出,除了标题栏不一样,其它内容还是loadedApp的代码执行的结果。

    探究

    在loadApp加载插件loadedApp后,在/data/data/宿主app包名/目录下多了一个文件夹,即app_loadedDexDir文件夹,在宿主app代码中有写File dexOutputDir = this.getDir("loadedDexDir", 0);,所以有这么一个文件夹,里面有个loadedApp.dex文件。

    image-20220630200138466

    我们可以将其提取出来研究

    1
    adb pull loadedApp.dex ./

    然后用JEB打开查看一下里面的内容。

    image-20220630200405746

    这里就是loadedApp的全部代码,所以,在加密插件方面,这里依然是可以知道插件APP的全部代码,为了安全,可以考虑添加一些检测机制,不然你的代码就很危险了。

    其次,这里只是简单的通过例子实现了加载apk,自己可以通过深入研究添加插件app的生命周期控制,以及一些services,资源访问问题等。

    参考链接

    下一篇:
    Hook神器-frida的安装和使用
    本文目录
    本文目录