Android APK免安装启动

本文描述了在Android应用中,需要使用附加功能的时候,通过下载APK ,且不用通过安装下载的附加APK的方式,唤起增值APK中的功能。且不用手动去管理附加APK中的Activity生命周期。

在讲诉具体实现之前说一下涉及的知识点
Java虚拟机启动流程
启动Java虚拟机,创建ClassLoader,将java字节码加载进入ClassLoader,随即找到入口函数,执行。当需要创建一个对象的时候,向Java虚拟机发送一个请求,Java虚拟机接收到请求以后,首先在内存中进行寻找,若存在,则解析class,找到相应的方法执行。若内存中不存在,则让ClassLoader对相应的.class文件通过import 路径进行加载到内存中,然后进行解析,找到对应的方法执行。(ClassLoader实际上为虚拟机的一个部分,Java虚拟机并不会一次性将java字节码中的所有class文件进行加载,是需要什么,在内存中寻找不到的时候,再通过ClassLoader将对应的.class文件通过路径方式,加载进入内存,供他人使用)。Android dalvik虚拟机对于ClassLoader的处理与Java虚拟机类似。

在说之前,先提出一个问题:
我们知道DexClassLoader加载的Activity是没有生命周期的,而我们知道dalvik对于类的查找以及加载流程了,那么我们是不是可以将我们加载的dex让Android虚拟机帮我们管理呢,因为虚拟机要加载类同样也是通过ClassLoader进行加载的。
**
这便有产生一个想法,Android程序运行过程中,所需要的类都是采用上述方式进行加载,那么可不可以找到虚拟机中的ClassLoader,将需要动态加载的dex或者APK文件进行加载,便于去寻找。

首先找到ZygoteInit.java文件 这个是创建应用进程的入口

static void invokeStaticMain(ClassLoader loader,
119             String className, String[] argv)
120             throws ZygoteInit.MethodAndArgsCaller {
121         Class<?> cl;
122 
123         try {
124             cl = loader.loadClass(className);
125         } catch (ClassNotFoundException ex) {
126             throw new RuntimeException(
127                     "Missing class when invoking static main " + className,
128                     ex);
129         }
130 
131         Method m;
132         try {//在此处调用ActivityThread.java的main方法
133             m = cl.getMethod("main", new Class[] { String[].class });
134         } catch (NoSuchMethodException ex) {
135             throw new RuntimeException(
136                     "Missing static main on " + className, ex);
137         } catch (SecurityException ex) {
138             throw new RuntimeException(
139                     "Problem getting static main on " + className, ex);
140         }
141 
142         int modifiers = m.getModifiers();
143         if (! (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers))) {
144             throw new RuntimeException(
145                     "Main method is not public and static on " + className);
146         }
147 
148         /*
149          * This throw gets caught in ZygoteInit.main(), which responds
150          * by invoking the exception's run() method. This arrangement
151          * clears up all the stack frames that were required in setting
152          * up the process.
153          */
154         throw new ZygoteInit.MethodAndArgsCaller(m, argv);
155     }

在此处创建了一个新的ActivityThread对象

public static void main(String[] args) {
        SamplingProfilerIntegration.start();

        // CloseGuard defaults to true and can be quite spammy.  We
        // disable it here, but selectively enable it later (via
        // StrictMode) on debug builds, but using DropBox, not logs.
        CloseGuard.setEnabled(false);

        Environment.initForCurrentUser();

        // Set the reporter for event logging in libcore
        EventLogger.setReporter(new EventLoggingReporter());

        Security.addProvider(new AndroidKeyStoreProvider());

        // Make sure TrustedCertificateStore looks in the right place for CA certificates
        final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
        TrustedCertificateStore.setDefaultUserDirectory(configDir);

        Process.setArgV0("<pre-initialized>");

        Looper.prepareMainLooper();

        ActivityThread thread = new ActivityThread();
        thread.attach(false);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }

        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

现在我们看一下ActivityThread实例中的变量

// These can be accessed by multiple threads; mPackages is the lock.
    // XXX For now we keep around information about all packages we have
    // seen, not removing entries from this map.
    // NOTE: The activity and window managers need to call in to
    // ActivityThread to do things like update resource configurations,
    // which means this lock gets held while the activity and window managers
    // holds their own lock.  Thus you MUST NEVER call back into the activity manager
    // or window manager or anything that depends on them while holding this lock.
    final ArrayMap<String, WeakReference<LoadedApk>> mPackages
            = new ArrayMap<String, WeakReference<LoadedApk>>();
    final ArrayMap<String, WeakReference<LoadedApk>> mResourcePackages
            = new ArrayMap<String, WeakReference<LoadedApk>>();
    final ArrayList<ActivityClientRecord> mRelaunchingActivities
            = new ArrayList<ActivityClientRecord>();

我们看见其中有mPackages,mResourcePackages, mRelaunchingActivities其他两个不用理会,我们关注mPackages,我们转而进入LoadedApk.java;去看看里面有什么东西。

/**
  75  * Local state maintained about a currently loaded .apk.
  76  * @hide
  77  */
  78 public final class LoadedApk {
  79 
  80     private static final String TAG = "LoadedApk";
  81 
  82     private final ActivityThread mActivityThread;
  83     private ApplicationInfo mApplicationInfo;
  84     final String mPackageName;
  85     private final String mAppDir;
  86     private final String mResDir;
  87     private final String[] mSplitAppDirs;
  88     private final String[] mSplitResDirs;
  89     private final String[] mOverlayDirs;
  90     private final String[] mSharedLibraries;
  91     private final String mDataDir;
  92     private final String mLibDir;
  93     private final File mDataDirFile;
  94     private final ClassLoader mBaseClassLoader;
  95     private final boolean mSecurityViolation;
  96     private final boolean mIncludeCode;
  97     private final boolean mRegisterPackage;
  98     private final DisplayAdjustments mDisplayAdjustments = new DisplayAdjustments();
  99     Resources mResources;
 100     private ClassLoader mClassLoader;
 101     private Application mApplication;
 102 
 103     private final ArrayMap<Context, ArrayMap<BroadcastReceiver, ReceiverDispatcher>> mReceivers
 104         = new ArrayMap<Context, ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>>();
 105     private final ArrayMap<Context, ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>> mUnregisteredReceivers
 106         = new ArrayMap<Context, ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>>();
 107     private final ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>> mServices
 108         = new ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>>();
 109     private final ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>> mUnboundServices
 110         = new ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>>();
 111 
 112     int mClientCount = 0;
 113 
 114     Application getApplication() {
 115         return mApplication;
 116     }

在上述的变量中我们发现有一个非final类型的ClassLoader对象,在这里楼主做了一些求证,为了证明该ClassLoader对象是我所需要的,我将日志输入进去,并编译了整个源码,运行了一个测试程序,发现这个的确是我所需要的ClassLoader。

因此到此为止,我们已经找到我们所需要的东西。

接下来我们说一下Android中的ClassLoader机制
大家都知道,Android对外开发的有2个ClassLoader, 一个叫做PathClassLoader,另外一个叫做DexClassLoader。他们都是继承BaseDexClassLoader,当我们实例化一个DexClassLoader的时候,

public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)

上述为DexClassLoader的构造函数, 传入的parent是做什么用途的呢 ?
一个ClassLoader的加载类的流程为,首先通过最顶层的classLoader中进行查找该classLoader中是否已经存在了该类,若没有,则在次一层的classLoader中进行查找,当上层的classLoader都没有找到该类的时候,则跑到最后一层中对该类进行查找,若找到则返回该类,若没有找到则返回ClassNotFound的异常。

一言以蔽之:加载一个类,首先在内存中查找,没有则通过dex文件进行路径查找该类,若找到,加载至内存,若没有则返回ClassNotFound异常。以下为classLoader的查找类函数。

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }

            if (clazz == null) {
                try {
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }

        return clazz;
    }

因此我们可以得出,当我们实例化一个DexClassLoader,并将其构造函数的parent设置为LoadedApk.java中的mClassLoader,并将其替换,这样不就可以实现让Android帮我们自动管理我们所动态加载的Activity了嘛。

因为这样并不会导致原来的类查找不到,因为我已经将原来载入了主APK的ClassLoader设置为我们替代的DexClassLoader的parent。这样当虚拟机去加载类的时候,同样还是可以加载原有的,同时又可以加载我们动态加载的dex。

到此为止,我们已经从理论上实现了动态加载Activity,并让Android本身帮助我们管理动态加载的Activity,并且带有生命周期。

以下是动态加载DEX的代码:

public class MainActivity extends AppCompatActivity {

    private static final String ACTIVITY_THREAD_CLASS_PATH = "android.app.ActivityThread";
    private static final String GET_CURRENT_ACTIVITY_THREAD_METHOD_NAME = "currentActivityThread";
    private static final String ACTIVITY_THREAD_PACKAGES = "mPackages";
    private static final String LOAD_APK_CLASS_PATH = "android.app.LoadedApk";
    private static final String LOAD_APK_CLASS_LOADER_FILED_NAME = "mClassLoader";

    private static final String APK_NAME = "app-debug.apk";
    private static final String PLUGIN_APK_PATH = Environment.getExternalStorageDirectory().getPath() + File.separator + APK_NAME;
    private static String PLUGIN_APK_INTENER_PATH;
    private static String DEX_OUT_PUT_PATH = Environment.getExternalStorageDirectory().getPath();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        DEX_OUT_PUT_PATH = getApplicationContext().getFilesDir().getPath();
        init();
        initLoadPlugin();
    }

    @Override
    public void onResume(){
        super.onResume();
        DynamicApplication.getApplication().resetResource();
    }

    private void init() {
        View button = findViewById(R.id.buttonPanel);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                DynamicApplication.getApplication().loadPlugInResources(PLUGIN_APK_INTENER_PATH);
                Intent intent = new Intent();
                intent.setClassName(MainActivity.this, "smither.gionee.com.plugin.PluginActivity");
                ResolveInfo info =  getPackageManager().resolveActivity(intent, 0);
                if (null != info) {
                    startActivity(intent);
                }
            }
        });
    }

    private void replaceApkClassLoader(DexClassLoader classLoader) throws Exception {
        Object currentActivityThread = RefInvoke.invokeStaticMethod(ACTIVITY_THREAD_CLASS_PATH,
                GET_CURRENT_ACTIVITY_THREAD_METHOD_NAME, new Class[]{}, new Object[]{});
        String currentPackageName = getPackageName();
        ArrayMap activityThreadPackages = (ArrayMap) RefInvoke.getFieldOjbect(ACTIVITY_THREAD_CLASS_PATH,
                currentActivityThread, ACTIVITY_THREAD_PACKAGES);

        WeakReference loadApk = (WeakReference) activityThreadPackages.get(currentPackageName);
        RefInvoke.setFieldOjbect(LOAD_APK_CLASS_PATH, LOAD_APK_CLASS_LOADER_FILED_NAME, loadApk.get(), classLoader);
    }

    private void initLoadPlugin() { 
        if (new File(PLUGIN_APK_PATH).exists()){
            try {
                String copyApkPath = copyDex().getPath();
                DexClassLoader classLoader = new DexClassLoader(copyApkPath,
                        DEX_OUT_PUT_PATH, null, getClassLoader());
                replaceApkClassLoader(classLoader);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }else {
            Toast.makeText(this, "PLUGIN_APK_PATH is not exists", Toast.LENGTH_SHORT).show();
        }

    }

    private File copyDex() throws IOException {
        File file = new File(getApplicationContext().getFilesDir() + File.separator + APK_NAME);
        File plugIn1 = new File(PLUGIN_APK_PATH);
        if (!file.exists()) {
            file.createNewFile();
        }

        if (plugIn1.length() != file.length()) {
            FileOutputStream outputStream = new FileOutputStream(file);
            FileInputStream fileInputStream = new FileInputStream(plugIn1);
            byte[] buffer = new byte[fileInputStream.available()];
            fileInputStream.read(buffer);
            fileInputStream.close();
            outputStream.write(buffer);
            outputStream.close();
        }
        PLUGIN_APK_INTENER_PATH = file.getPath();
        return file;
    }
}

需要动态加载的APK的代码我就不贴出来了,那里面只有一个空的Activity; 名字为smither.gionee.com.plugin.PluginActivity

现在可以去运行了,哈哈,不过你突然发现,我明明可以加载Activity,但是跳转到dex中的Activity为何还会报错?

这是因为当你用DexClassLoader进行加载APK的时候,并不会将你的AndroidManifest文件进行加载,因此你需要在主程序的AndroidManifest中声明该Activity。

接下来你就可以启动该Activity了。

接下来又产生一个新的问题

你会发现View不见了,或者跳转到的Acitivty的View并非你所指定的View

这是因为采用DexClassLoader动态加载进来的APK并没有将资源文件加载进来,而当插件APK中调用setContentView(int layoutId),所访问的R文件其实是调用插件中的R文件,而插件中的R文件,与宿主本身的R文件不一样,因此当引用插件R文件中的资源ID的时候,实际上是通过该资源ID去宿主中去寻找资源文件,这样肯定是找不到的。因为不同的APK的R资源都是不一样的。所以是无法直接使用R.layout.customer_layout来访问的。因为这样所访问的资源ID其实是不存在的。

那么有没有办法呢?
首先我们可以确定通过DexClassLoader加载进来的APK中的Activity并没有产生新的Application, 而跳转到插件中的Activity实际上还是使用的同一个Application。因为一个Application的产生需要通过Zygote fork出一个新的进程,进而去加载APK资源,产生Application。通常正常情况下,一个应用是只有一个Application,当我们通过上述方式获取Application的时候,其实还是获取到宿主的Application。

因此在此时可以通过重写Application.getResource()等方法,当切换到插件工程中的Activity的时候,可以将对应的在Application中切换资源的获取。在设置View,获取layout,String等资源文件的时候,采用Application.getResource(),而这个实际上获取到的就是我们手动加载进来的插件工程中的资源文件。

接下来看一下代码的实现:
首先看一下宿主工程的Application

public class DynamicApplication extends Application{

    private Resources mResource;
    private Resources.Theme mTheme;
    private AssetManager mAssetManager;
    private static DynamicApplication mApplication;

    public static DynamicApplication getApplication(){
        return mApplication;
    }

    public void onCreate() {
        super.onCreate();
        mApplication = this;
    }

    public void resetResource(){
        mResource = super.getResources();
        mTheme = super.getTheme();
    }

    @Override
    public Resources getResources() {
        return mResource == null ? super.getResources() : mResource;
    }

    @Override
    public Resources.Theme getTheme() {
        return mTheme == null ? super.getTheme() : mTheme;
    }

    public void loadPlugInResources(String dexPath) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPathMethod = assetManager.getClass().getMethod("addAssetPath", String.class);
            Object obj = addAssetPathMethod.invoke(assetManager, dexPath);
            mAssetManager = assetManager;
            Resources superResources = super.getResources();
            mResource = new Resources(assetManager, superResources.getDisplayMetrics(), superResources.getConfiguration());
            mTheme = mResource.newTheme();
            mTheme.setTo(super.getTheme());

        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

接下来是插件工程中Activity获取资源的方式需要稍微修改一下:

View view = LayoutInflater.from(getApplication()).inflate(R.layout.activity_plugin, null);

  view = LayoutInflater.from(getApplication()).inflate(getApplication().getResources().getLayout(R.layout.activity_plugin), null);
        /**
         * 该处的两种加载XML layout的方式都是可行的。效果是一样的  走的也是同一套流程 在inflate函数中同样也调用了getResources().getLayout()方式。
         */
        setContentView(view);

在此处我有试过将获取的日志进行输出(可以参看下面的日志信息):

public class PluginActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        /**
         * 当该apk由另外一个APK启动的时候,此时获取的getApplication()实际上为启动该ACTIVITY的应用的Application。
         * 且我在主工程的Application已经对该APK的资源进行了加载,且对getResource等方法进行了重写。因此当调用getApplication(),
         * 实际上获取的是主工程的DynamicApplication,因为插件的Application并未启动,而在DynamicApplication中又对getResource进行了重写,
         * 因此此刻获取到的实际上是插件工程中的资源文件。这样便可以实现调用
         */
        Log.v("PluginActivity", "onCreate");
        /**
         * 该处返回的所指定的Context所生成的LayoutInflater 可以参看LayoutInflater的构造函数
         * Obtains the LayoutInflater from the given context.
         * public static LayoutInflater from(Context context) {
         * LayoutInflater LayoutInflater =
         *      (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
         * if (LayoutInflater == null) {
         *   throw new AssertionError("LayoutInflater not found.");
         * }
         * return LayoutInflater;
         *}
         */
        LayoutInflater inflater = LayoutInflater.from(getApplication());
        Log.v("PluginActivity", "LayoutInflater inside Context:" + inflater.getContext() + " Application:" + getApplication());
        Log.v("PluginActivity", "LayoutInflater inside Context  == getApplication() :" + (inflater.getContext() == getApplication()));
        /**
         *  public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
         *      final Resources res = getContext().getResources();
         *      if (DEBUG) {
         *          Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
         *           + Integer.toHexString(resource) + ")");
         *   }

         *  final XmlResourceParser parser = res.getLayout(resource);
         *  try {
         *      return inflate(parser, root, attachToRoot);
         *  } finally {
         *      parser.close();
         *  }
         *  }
         *  以下为输出日志LayoutInflater inside Context:smither.gionee.com.dynamicmain.DynamicApplication@e6b7a3 Application:smither.gionee.com.dynamicmain.DynamicApplication@e6b7a3
         *  从上文可以看出由于Context为相同的  因此其调用的 getResources实际上为在DynamicApplication@重写的getResources,因此能够正常加载
         *  同理可以获取String等资源
         *  为何一样的原因请参看https://docs.google.com/document/d/10EYlyuxDw1KPy7LJlGtgMz69gwMO-pjDHS2GRtppvZg/edit?pref=2&pli=1
         *
         *  下列为下面所有日志打印的输出:
         *  01-15 08:47:31.027 V/PluginActivity(14208): onCreate
         *  01-15 08:47:31.028 V/PluginActivity(14208): LayoutInflater inside Context:smither.gionee.com.dynamicmain.DynamicApplication@e6b7a3 Application:smither.gionee.com.dynamicmain.DynamicApplication@e6b7a3
         *  01-15 08:47:31.028 V/PluginActivity(14208): LayoutInflater inside Context  == getApplication() :true
         *  01-15 08:47:31.030 V/PluginActivity(14208): LayoutInflater.from(getApplication()).inflate(R.layout.activity_plugin, null):false
         *  01-15 08:47:31.040 V/PluginActivity(14208): LayoutInflater.from(getApplication()).inflate(getResources().getLayout(R.layout.activity_plugin), null):false
         *  01-15 08:47:31.040 V/PluginActivity(14208): Current Package Name:smither.gionee.com.dynamicmain Real package name:smither.gionee.com.plugin
         *  01-15 08:47:31.040 V/PluginActivity(14208): getApplication Current Package Name:smither.gionee.com.dynamicmain Real package name:smither.gionee.com.plugin
         *  01-15 08:47:31.040 V/PluginActivity(14208): getResources().getString(R.string.app_name):DynamicMain real app name:Plugin
         *  01-15 08:47:31.040 V/PluginActivity(14208): getApplication getResources().getString(R.string.app_name):Plugin real app name:Plugin
         */
        View view = LayoutInflater.from(getApplication()).inflate(R.layout.activity_plugin, null);
        Log.v("PluginActivity", "LayoutInflater.from(getApplication()).inflate(R.layout.activity_plugin, null):" + (null == view ? true : false));

        view = LayoutInflater.from(getApplication()).inflate(getApplication().getResources().getLayout(R.layout.activity_plugin), null);
        /**
         * 该处的两种加载XML layout的方式都是可行的。效果是一样的  走的也是同一套流程 在inflate函数中同样也调用了getResources().getLayout()方式。
         */
        setContentView(view);
        Log.v("PluginActivity", "LayoutInflater.from(getApplication()).inflate(getResources().getLayout(R.layout.activity_plugin), null):" + (null == view ? true : false));
     //   setContentView(R.layout.activity_plugin);
        Log.v("PluginActivity", "Current Package Name:" + getPackageName() + " Real package name:smither.gionee.com.plugin");
        Log.v("PluginActivity", "getApplication Current Package Name:" + getApplication().getPackageName() + " Real package name:smither.gionee.com.plugin");
        Log.v("PluginActivity", "getResources().getString(R.string.app_name):" + getResources().getString(R.string.app_name) + " real app name:Plugin");
        Log.v("PluginActivity", "getApplication getResources().getString(R.string.app_name):" + getApplication().getResources().getString(R.string.app_name) + " real app name:Plugin");

    }
}

此时,就可以正常使用插件工程中的Activity了。

好了,此次免安装方式动态加载APK就讲到此处。