Take a look at GW

【Android】Android动态加载Jar、APK的实现

本文介绍Android中动态加载Jar、APK的实现。而主要用到的就是DexClassLoader这个类。大家都知道Android和普通的Java虚拟机有差别,它只能加载经过处理的dex文件。而加载这个dex文件可以通过DexClassLoader 和 PathClassLoader 两个类来实现这个方法。然而PathClassLoader只能加载已经安装到Android系统中的apk文件。接下来,会介绍在Android中如何动态加载Jar、如何加载未安装的APK,如何加载已经安装的APK。

1.Android如何动态加载Jar

动态加载Jar主要是用于在APP的热更新、插件化开发方面,在进行加载之前,首先需要生成Jar文件。测试的jar包定义了一个接口和一个实现类。需要注意定义接口的步骤是必不可少的,在后面利用反射加载的时候就要利用到这个接口。

定义ILoader接口:

package com.example.interf;

public interface ILoader {
    public String sayHi();
}
ILoader.java

定义JarLoader类:

package com.example.interf;

public class JarLoader implements ILoader {
    @Override
    public String sayHi() {
        return "来自动态加载的Jar";
    }
}
JarLoader.java

然后打包为Jar文件

这里笔者导出为Loader.jar文件。有一点需要注意,就是不要把ILoader.jar接口打包进去,因为后期可能会包错:  java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation ,出现这个错误的原因就是打包的Jar中有接口,然后在下面的文件中又定义了接口,出现了两个接口文件,所以报错。如果这里把ILoader接口打包进去,那么在下面的测试中就不要再定义相同的ILoader接口了。

到这里我们就把Jar文件打包成功了,接下来了需要把这个Jar文件用dx工具进行处理,dx工具在Android SDK 的tools中已经提供了,一般在android-SDK/build-tools目录下。

将上面的Loader.jar文件拷贝一份到dx同级的目录下,然后执行如下命令:

dx --dex --output=Loader_dex.jar Loader.jar  

 然后将生成的Loader_dex.jar文件,拷贝到手机的SD根目录下面(手机SD的根目录就是:/storage/emulated/0,读者也可以使用 Environment.getExternalStorageDirectory() 查看)

接下来就可以使用如下的代码进行加载:

其中ILoader.java接口与Loader.jar中的ILoader接口保持一直。

package com.example.test;

import java.io.File;

import com.example.interf.ILoader;

import dalvik.system.DexClassLoader;

import android.os.Bundle;
import android.os.Environment;
import android.app.Activity;
import android.util.Log;
import android.widget.Toast;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        loadJar();
    }
    
    /**
     * @Title LoadJar
     * @Description  项目工程中必须定义接口(包名都要一致), 而被引入的第三方jar包实现这些接口,然后进行动态加载 。 
     *                 相当于第三方按照接口协议来开发, 使得第三方应用可以以插件的形式动态加载到应用平台中。 
     * @return void
     */
    private void loadJar(){
        File dexoutputdir = getDir("dex1",0);
        String dexPath = Environment.getExternalStorageDirectory().toString() + File.separator + "Loader_dex.jar";
        DexClassLoader loader = new DexClassLoader(dexPath,dexoutputdir.getAbsolutePath(),null,getClassLoader());
        
        try {
            Class clz = loader.loadClass("com.example.interf.JarLoader");
            ILoader iShowToast = (ILoader) clz.newInstance();
            Toast.makeText(this,iShowToast.sayHi(),Toast.LENGTH_LONG).show();
        } catch (Exception e){
             Log.d("dd",e.toString());
        }
    }  
}
MainActivity.java

在这个类中定义的核心方法是loadJar()

    private void loadJar(){
        File dexoutputdir = getDir("dex1",0);
        String dexPath = Environment.getExternalStorageDirectory().toString() + File.separator + "Loader_dex.jar";
        DexClassLoader loader = new DexClassLoader(dexPath,dexoutputdir.getAbsolutePath(),null,getClassLoader());
        
        try {
            Class clz = loader.loadClass("com.example.interf.JarLoader");
            ILoader iShowToast = (ILoader) clz.newInstance();
            Toast.makeText(this,iShowToast.sayHi(),Toast.LENGTH_LONG).show();
        } catch (Exception e){
             Log.d("dd",e.toString());
        }
    }  

接下来笔者解释一下上面这个方法中核心类DexClassLoader。

此处需要注意DexClassLoader的四个参数:
参数1 dexPath:待加载的dex文件路径,如果是外存路径,一定要加上读外存文件的权限( <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> ),否则会报与报错,Android4.4 KitKat及以后的版本需要此权限,之前的版本不需要该权限。

参数2 optimizedDirectory:解压后的dex存放位置,此位置一定要是可读写且仅该应用可读写(安全性考虑),所以只能放在data/data下。本文getDir(“dex1”, 0)会在/data/data/**package/下创建一个名叫”app_dex1“的文件夹,其内存放的文件是自动生成Loader_dex.dex;需要注意,data/data文件夹只有在手机root之后,才看得到。

参数3 libraryPath:指向包含本地库(so)的文件夹路径,可以设为null。


参数4 parent:父级类加载器,一般可以通过Context.getClassLoader获取到,也可以通过ClassLoader.getSystemClassLoader()取到。

效果图:

到这里动态加载Jar就结束了。笔者接下来总结一下思路,首先把jar文件经过dx工具处理,然后把处理后的文件放到手机的SD根目录下面,然后利用反射加载调用方法。上面其实只是实现热更新的一半,加载的Jar文件完全可以从服务器下载手机后,然后再在手机端加载,这样可以对手机上的APP进行实时的更新以及防止反编译。

2.如何加载未安装的APK

 上面介绍了如何动态加载jar文件,接下来介绍如何加载未安装的APK。

首先新建一个Android项目:

定义一个接口ISayHello.java

package com.example.loaduninstallapkdemo;

public interface ISayHello {
      public String sayHello();
}
ISayHello.java

然后新建Activity,实现ISayHello接口:

package com.example.loaduninstallapkdemo;

import android.os.Bundle;
import android.app.Activity;

public class MainActivity extends Activity implements ISayHello{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public String sayHello() {
        return "Hello, this apk is not installed";
    }
}
MainActivity.java

然后把该工程的APK拷贝到手机的SD根目录下面,

接下来就可以使用如下的代码进行动态加载了,下面的加载过程和上面的类似,只是不再需要定义接口了。

package com.example.test;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

import dalvik.system.DexClassLoader;

import android.os.Bundle;
import android.os.Environment;
import android.app.Activity;
import android.view.Menu;
import android.widget.Toast;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        loadUnInstallAPK();
    }
    
    private void loadUnInstallAPK(){
           String path = Environment.getExternalStorageDirectory() + File.separator;  
           String filename = "UninstallApkActivity.apk";  
      
           // 4.1以后不能够将optimizedDirectory设置到sd卡目录, 否则抛出异常.  
           File optimizedDirectoryFile = getDir("dex", 0) ;  
           DexClassLoader classLoader = new DexClassLoader(path + filename, optimizedDirectoryFile.getAbsolutePath(),  
                                                            null, getClassLoader());  
      
           try {  
            // 通过反射机制调用, 包名为com.example.loaduninstallapkdemo, 类名为MainActivity  
               Class mLoadClass = classLoader.loadClass("com.example.loaduninstallapkdemo.MainActivity");  
               Constructor constructor = mLoadClass.getConstructor(new Class[] {});  
               Object testActivity = constructor.newInstance(new Object[] {});  
                 
               // 获取sayHello方法  
               Method helloMethod = mLoadClass.getMethod("sayHello", null);  
               helloMethod.setAccessible(true);  
               Object content = helloMethod.invoke(testActivity, null);  
               Toast.makeText(MainActivity.this, content.toString(), Toast.LENGTH_LONG).show();  
                 
           } catch (Exception e) {  
               e.printStackTrace();  
           }  
    }
}
MainActivity.java

效果图:

 

3.如何加载已经安装的APK

 在介绍了如何动态加载jar,加载未安装的APK后,接下来介绍如何加载已经安装的APK,

首先将制作一个简单的APK,然后把它安装的手机上面。

package com.example.installapkdemo;

import android.os.Bundle;
import android.app.Activity;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}
MainActivity.java

 

将该APK安装到手机后,接下来就可以进行加载了。

同样和加载未安装的APK类似,项目中也不需要定义接口。

package com.example.test;

import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.util.Log;
import android.view.Menu;
import android.widget.Toast;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadInstalledApk();
    }
    private void loadInstalledApk(){
        try {  
            String pkgName = "com.example.installapkdemo";  
            Context context = createPackageContext(pkgName,  
                    Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE) ;  
              
            // 获取动态加载得到的资源  
            Resources resources = context.getResources() ;  
            // 获取该apk中的字符串资源"hello_world", 并且toast出来,apk换肤的实现就是这种原理  
            String toast = resources.getString(resources.getIdentifier("hello_world", "string", pkgName) ) ;  
            Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show() ;  
              
            Class cls = context.getClassLoader().loadClass(pkgName + ".MainActivity");  
            // 跳转到该Activity  
            startActivity(new Intent(context, cls)) ;  
        } catch (NameNotFoundException e) {  
            e.printStackTrace();  
        }catch (ClassNotFoundException e) {  
            Log.d("", e.toString()) ;  
        }  
    }
}
MainActivity.java

效果图:

 

原文链接:

Android动态加载jar,apk的实现

 

posted @ 2018-01-18 11:06  HDWK  阅读(2571)  评论(0编辑  收藏  举报