安卓动态加载技术

栏目: IOS · Android · 发布时间: 7年前

内容简介:安卓动态加载技术

何为动态加载技术

在谈动态加载之前,先来看一个场景,很多App产品的启动页会在一些重要的节日将原本平常的启动页图片更换为在某个节日相应的应景图片,如在国庆节时百度地图启动页面会是关于国庆祝福的图片,在过年的时候会是xx公司给大家拜年的图片,这样不仅能够提高用户体验,让App不在单调,另一方面,也给了App以温度,记得腾讯QQ在我的生日那天启动页面是祝我生日快乐的页面,这样能够让用户感觉到这个App的温暖,(虽然只是一个简短的启动页面的祝福,虽然腾讯没在我的生日给我免费开个会员,只是假惺惺的说了句祝福的话,但实际上这反映的是该App背后的企业对用户体验的重视,说句实话,在中国互联网企业中,个人觉得腾讯的产品是做的最好的,没有之一)很显然,这些启动图片是不可能在发布Apk的时候就直接打包进去的,而是通过网络从服务端下载下来,然后按需显示出来的,再比如很多App都会有换肤功能,比如QQ会有很多个性设置模块,有些皮肤甚至是以apk插件的形式存在于服务端,当用户觉得该皮肤不错,点击更换皮肤的时候客户端才从服务端将该皮肤插件apk下载下来,然后显示在客户端。这就是动态加载技术,即客户端能够在运行时动态的调用外部代码从而实现在不更改客户端代码的前提下更新客户端的App的功能,其本质是客户端的类加载器加载从服务端下载下来的apk、dex、jar(必须包含dex文件)文件中的资源(如类的代码,图片资源等)供客户端App使用。

动态加载技术的好处

  • 能够动态加载资源文件,如将某些不常用的资源以额外的apk或者jar包的形式提供,而不需要一起打包在主apk中,实现按需使用,从而可以减小apk的体积,如换肤功能,某些皮肤可能用户永远都不会使用,因此不需要打包在主apk中
  • 能够在不修改已经发布的apk代码的前提下动态增加或者更新该apk的功能,如插件化技术,将某些功能以插件的形式提供,这样加载不同的插件就可以实现更新不同的功能

下面就以前面说到的腾讯QQ换肤功能为例分析换肤中的动态加载资源的原理,然后带领大家实现该功能!

换肤原理

前面说过,一般App的换肤功能是将皮肤以插件的形式存放于服务端,当用户点击换肤之后客户端App会从服务端去下载该插件apk到本地,然后通过动态加载技术使用该apk中的资源,包括类的代码和图片等。在这里就涉及到两个概念,宿主App和插件App,宿主App指的是调用插件apk的App,如QQ换肤功能案例中的QQ,而插件App指的是提供资源供客户端App调用的App,如QQ换肤功能案例中的某一个皮肤插件(通常是以apk或者dex文件格式的形式提供的)。

那么这里就涉及到一个问题就是插件apk是未安装的,(当然我们也可以在用户下载了该皮肤包插件后弹出一个安装界面让用户选择安装,但是这样很明显用户体验太差),我们知道在安卓中使用资源文件需要Context对象,但是apk未安装,没走安卓系统中apk的初始化启动过程,因此不能够得到该未安装的apk的Context,那么我们只能退而求其次,看能否得到未安装apk的Resource对象?因为我们通过Context来访问资源文件本质上是通过getResources()函数来访问的,而该函数实际上返回的是Resources对象,因此我们如果能够直接得到未安装apk的Resources的话,就能够操作其资源文件了。因此我们先看一下Resources类的构造函数

public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
    throw new RuntimeException("Stub!");
}

可以看到Resources类的构造函数包含三个参数,其中第一个参数为AssetManager,大家可能回想这很简单啊,我们实例化一个AssetManager对象传进去不就可以了,事实上不是这么简单的,还是和前面的原因一样,安卓系统中的很多对象的使用是需要走apk初始化启动过程的,不是简单的创建一个 java 对象就可以使用的,因为每个APK文件在进程中都对应有一个全局的Resourses对象以及一个全局的AssetManager对象,也就是说如果只是简单的实例化一个AssetManager对象传进去,那么这个对象仅仅只是一个普通的java类而已,不具备安卓中全局AssetManager对象的功能,因此也就不具备使用插件apk中资源的功能,因为该AssetManager根本就没走apk初始化启动过程,因此和插件apk无任何关系。但是在AssetManager类中我们看到了addAssetPath函数:代码如下:

/**
 * Add an additional set of assets to the asset manager.  This can be
 * either a directory or ZIP file.  Not for use by applications.  Returns
 * the cookie of the added asset, or 0 on failure.
 * {@hide}
 */
public final int addAssetPath(String path) {
    return  addAssetPathInternal(path, false);
}

从注释可以看到该函数的作用是将额外的assets添加到AssetManager中,其参数是一个文件夹或者zip文件路径,而apk文件本质上就是一个zip包,事实上走了安卓系统初始化过程的apk之所以能够通过AssetManager调用资源也是系统调用了该函数建立起了资源与apk之间对应关系。但是从注释可以看到该函数是隐藏的,因此不能够直接调用,但是我们可以通过反射机制来调用该函数。代码如下:

AssetManager assetManager = AssetManager.class.newInstance();
//通过反射调用方法addAssetPath(String path),将插件Apk文件的添加进AssetManager中,
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, pluginApkPath+File.separator+apkName);

首先创建一个AssetManager对象,然后通过反射调用addAssetPath函数将apk资源文件加载到对应的AssetManager中,这样我们构建的AssetManager才不是一个普通的java对象,而是安卓中的可以访问资源的AssetManager对象,然后就可以按照前面说的构造Resources对象了。代码如下:

AssetManager assetManager = AssetManager.class.newInstance();
//通过反射调用方法addAssetPath(String path),将插件Apk文件的添加进AssetManager中,
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, pluginApkPath+File.separator+apkName);
Resources superRes = this.getResources();
Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(),
        superRes.getConfiguration());

这样就得到了未安装的插件apk中的全局Resources对象了,然后就可以使用该对象像操作已经安装了的apk那样使用未安装的插件apk中的资源了。因此接下来就是如何获取插件apk中的资源的id,因为我们知道在宿主apk中访问资源都是通过R文件形式来访问的,说白了就是如何加载插件apk中的R类,这个就是类加载器的原理了,和java中的类加载类似,在安卓中可以通过DexClassLoader类来加载未安装的apk,当然如果是dex文件的话还可以使用PathClassLoader这个类,这两个类的区别在于DexClassLoader可以传入包含dex的apk或者jar包路径作为参数,而PathClassLoader只能传入dex路径或者已安装的apk路径(已经安装的apk存在缓存的dex文件),因此对于未安装的apk我们需要使用DexClassLoader来加载类,该类的定义如下:

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

其每个参数的含义如下:

  • dexPath:包含dex文件的路径,如apk或者包含dex文件的jar包
  • optimizedDirectory:这个是从apk中释放出的dex文件的保存路径
  • librarySearchPath:顾名思义lib搜素路径,一般为null
  • parent:父加载器

得到DexClassLoader对象之后我们就可以使用loadClass函数来加载我们要加载的类,如换肤案例中,我们需要获得换肤图片的id,那么我们需要加载R$mipmap类(android studio项目工程图片资源一般保存在mipmap中),然后就可以通过反射获得该类中的一些字段值,代码如下:

Class<?> clazz = dexClassLoader.loadClass(pkgName + ".R$mipmap");
Field field = clazz.getDeclaredField("skin");//得到名为skin的这张图片在R文件中对应的域值
int resId = field.getInt(R.id.class);//得到图片id

这样我们就得到了插件apk中换肤图片的id,然后就可以像操作已安装的apk那样通过id来设置皮肤图片了。

换肤实例

前面的理论知识讲解完了,那么接下来就通过实战来实现换肤功能。在android studio中创建一个工程命名为ChangeSkin,这里为了简单起见,主界面只包括一个ImageView和一个Button,一个用来显示皮肤图片,一个用来点击切换图片。如图

安卓动态加载技术

然后创建一个插件apk工程,命名为skinplugin,当然插件apk在创建的时候可以选择无Activity,因为只需要提供一些资源而已,无需界面。在一般的换肤功能app中,皮肤插件都是从服务端通过网络下载至本地然后使用,这里为了简单起见,直接把插件apk放到宿主apk的assets目录下,事实上这和从网络下载至本地无实质性差异,整个宿主apk代码如下:

public class MainActivity extends AppCompatActivity {
 
    private Button changeSkinBtn;
    private static final String pluginApkPath= Environment.getExternalStorageDirectory().getPath()+File.separator+"plugin";;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        changeSkinBtn= (Button) findViewById(R.id.btn_changeskin);
        changeSkinBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                changeSkin(pluginApkPath);
            }
        });
 
    }
 
    private void changeSkin(String apkPath) {
 
        downloadApk(pluginApkPath,"skinplugin.apk");
 
        PackageManager pm = getPackageManager();
        PackageInfo pkgInfo = pm.getPackageArchiveInfo(apkPath+File.separator+"skinplugin.apk", PackageManager.GET_ACTIVITIES);
        if (pkgInfo != null) {
            //得到插件apk的包名,用于dexClassLoader去加载相应的类
            String pkgName  = pkgInfo.applicationInfo.packageName;
 
            File optimizedDirectoryFile = getDir("dex", MODE_PRIVATE);
            DexClassLoader dexClassLoader = new DexClassLoader(pluginApkPath + File.separator +"skinplugin.apk", optimizedDirectoryFile.getPath(),
                    null, ClassLoader.getSystemClassLoader());
            try {
                Class<?> clazz = dexClassLoader.loadClass(pkgName + ".R$mipmap");
                Field field = clazz.getDeclaredField("skin");//得到名为skin的这张图片在R文件中对应的域值
                int resId = field.getInt(R.id.class);//得到图片id
                Resources mResources = getPluginResources("skinplugin.apk");//得到插件apk中的Resource
                if (mResources != null) {
                    //通过插件apk中的Resource得到resId对应的资源
                    Log.i("test","开始换肤");
                    findViewById(R.id.imageView).setBackgroundDrawable(mResources.getDrawable(resId));
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            }
        }
    }
 
    //模拟从服务端下载插件apk的过程,这里只是简单的将apk从assets目录拷贝至apkpluginPath路径
    private void downloadApk(String apkpluginPath,String apkName)
    {
        File file = new File(apkpluginPath);
        if (!file.exists()) {
            file.mkdirs();
        }
        File apk = new File(apkpluginPath + File.separator + apkName);
        try {
            if(apk.exists()){
               Log.i("test","插件apk已存在");
                return;
            }
 //           Log.i("test","开始下载皮肤插件");
            FileOutputStream fos = new FileOutputStream(apk);
            InputStream is = getResources().getAssets().open(apkName);
            BufferedInputStream bis = new BufferedInputStream(is);
            int len = -1;
            byte[] by = new byte[1024];
            while ((len = bis.read(by)) != -1) {
                fos.write(by, 0, len);
                fos.flush();
            }
            fos.close();
            is.close();
            bis.close();
 //           Log.i("test","皮肤插件下载完成");
 
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
    private Resources getPluginResources(String apkName) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            //通过反射调用方法addAssetPath(String path),将插件Apk文件的添加到AssetManager中,
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, pluginApkPath+File.separator+apkName);
            Resources superRes = this.getResources();
            Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(),
                    superRes.getConfiguration());
            return mResources;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
 
}

整个代码注释很详细,原理都是前面介绍过的,大家应该能够看懂,然后运行工程,会出现上图所示的界面,主界面显示QQ图像,下面是换肤按钮,点击换肤按钮,可以看到憨态可掬的企鹅被长相甜美的妹纸取代了哦,如图:

安卓动态加载技术

而妹纸图是在插件apk中的,也就是说已经达到了不安装插件apk,而在宿主apk中使用插件apk的资源的效果哦,也就达到了动态加载资源的目的。当然这里只是动态加载资源文件,事实上还可以动态加载Activity,不过这个比较复杂,关于动态加载技术的应用插件化技术目前在github上也可以找到一些著名的开源框架,如360的DroidPlugin,感兴趣的同学可以去看下。

源码下载地址:

本地下载

注:本文首次发表于www.huqi.tk,谢绝转载,如需转载,请注明出处:www.huqi.tk


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

社群运营的艺术

社群运营的艺术

查尔斯·沃格 / 靳婷婷 / 华夏出版社 / 2017-7 / 42

社群存续的秘密,长期以来只有少数人知道,比如佛陀、耶稣及其弟子。 回溯3000年社群史,《社群运营的艺术》作者查尔斯•沃格总结了有归属感社群的七大原则。 在前互联网时代,七原则曾经造就伟大社群。在人人互联时代,应用七原则的社群将更繁荣。 本书作者耶鲁大学神学硕士查尔斯•沃格研究人类社会3000年的历史,结合个人亲身操作经历,提出了七条历经时间考验的原则:界限原则、入会原则、仪式原......一起来看看 《社群运营的艺术》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具