随着Html5技术成熟,轻应用越来越受欢迎,特别是其更新成本低的特点。与Native App相比,Web App不依赖于发布下载,也不需要安装使用,兼容多平台。目前也有不少Native App使用原生嵌套WebView的方式开发。但由于Html渲染特性,其执行效率不及Native App好,在硬件条件不佳的机子上流畅度很低,给用户的体验也比较差。反观Native App,尽管其执行效率高,但由于更新频率高而导致频繁下载安装,这一点也令用户很烦恼。本文参考java虚拟机的类加载机制,以及网上Android动态加载jar的例子,提出一种不依赖于重新安装而更新Native App的方式。

目的:利用Android类加载原理,实现免安装式更新Native App

1. 先回顾Java动态加载类的原理

实现一个Java应用,使用动态类加载,从外部jar中加载应用的核心代码。

制作一个ClassLoader,提供读取类的方法

 1 package com.kavmors.classloadtest;
 2
 3 import java.net.URL;
 4 import java.net.URLClassLoader;
 5
 6 import com.kavmors.classes.RemoteEntry;
 7
 8 public class RemoteClassLoader {
 9     /**
10      * 读取一个类,并返回实例
11      * @param jarPath jar包的地址
12      * @param classPath 类所在的地址(包括package名)
13      * @return 继承RemoteEntry接口的实体类实例,失败则返回null
14      */
15     public static RemoteEntry load(String jarPath, String classPath) {
16         URLClassLoader loader;
17         try {
18             loader = new URLClassLoader(new URL[]{new URL(jarPath)});
19             Class<?> c = loader.loadClass(classPath);
20             RemoteEntry instance = (RemoteEntry)c.newInstance();
21             loader.close();
22             return instance;
23         } catch (Exception e) {
24             e.printStackTrace();
25             return null;
26         }
27     }
28 }

制作一个供核心代码继承的接口。这个接口很简单,只有一个execute方法。

1 package com.kavmors.classes;
2
3 import com.kavmors.classloadtest.Main;
4
5 public interface RemoteEntry {
6     public void execute(Main main);
7 }

其中的Main类如下,是整个程序的主入口

 1 package com.kavmors.classloadtest;
 2
 3 import com.kavmors.classes.RemoteEntry;
 4
 5 public class Main {
 6     //这里定义核心代码所在类的包名+类名
 7     private final static String classPath = "com.kavmors.classes.MainEntry";
 8     //这里定义jar包的地址
 9     private final static String jarPath = "file:D:/MainEntry.jar";
10
11     //提供一个Main类的成员方法
12     public void printTime() {
13         System.out.println(System.currentTimeMillis());
14     }
15
16     //主入口在这里
17     public static void main(String[] args) {
18         Main main = new Main();
19         RemoteEntry entry = RemoteClassLoader.load(jarPath, classPath);
20         if (entry!=null) entry.execute(main);    //执行核心代码
21     }
22 }

从以上代码看,RemoteClassLoader.load从jarPath读取了MainEntry.jar,然后从jar包中读取了MainEntry类并返回了该类的实例,最后运行实例中execute方法。到此应用的框架就制作好了,可以把以上代码打包成Runnable jar,命令为RemoteLoader.jar,方便后面的测试。

接下来,需要生成MainEntry,继承RemoteEntry接口。MainEntry里的就是核心代码。

 1 package com.kavmors.classes;
 2
 3 import com.kavmors.classloadtest.Main;
 4
 5 public class MainEntry implements RemoteEntry {
 6     @Override
 7     public void execute(Main main) {
 8         System.out.println("Execute MainEntry.execute");
 9         main.printTime();
10     }
11 }

以上,实现了接口中execute方法,并调用了Main类中的成员方法。把这个Class打包成jar,命名为MainEntry.jar,路径为D:/MainEntry.jar。

现在测试一下,执行java -jar RemoteLoader.jar,结果在控制台中打印"Execute MainEntry.execute和时间戳。由于MainEntry继承了RemoteEntry,RemoteClassLoader.load返回的相当于MainEntry类的实例,所以执行了其中execute方法。注意RemoteLoader.jar中是没有MainEntry这个类的,这个类是在MainEntry.jar中定义的。

以上仅用URLClassLoader实现动态加载,原理详见参考资料[1]

2. Android动态类加载框架

以上例子中,程序的主入口与核心代码进行了分离。如果把RemoteClassLoader.jar看成安装在机子上的Native App,MainEntry.jar看成远程服务器上的文件,那么对于每次更新,只需把MainEntry.jar更新后部署在服务器上就可以了,Native App不需要任何修改。根据这种想法,可以实现不依赖于重新安装的更新方式。

在JVM上,使用URLClassLoader可以调用本地及网络上的jar,把jar中的class读取出来。而在安卓上,类生成的概念与JVM不完全一样[2]。Dalvik将编译到的.class文件重新打包成dex类型的文件,因此也有自己的类加载器DexClassLoader,只需要把上面例子的URLClassLoader换成DexClassLoader就可以。

考虑到现实开发的场景,在首次启动应用或需要更新的时候从服务器下载jar,存到本地,不需要更新的时候就直接使用本地的jar。这样,首先需要一个操作jar的类,用来判断jar是否存在,以及处理创建、删除、下载的任务。

  1 package com.kavmors.remoteloader;
  2
  3 import java.io.File;
  4 import java.io.FileOutputStream;
  5 import java.io.IOException;
  6 import java.io.InputStream;
  7 import java.io.OutputStream;
  8 import java.net.URL;
  9 import java.net.URLConnection;
 10
 11 import android.os.AsyncTask;
 12
 13 public class JarUtil {
 14     private OnDownloadCompleteListener mListener;
 15     private String jarPath;
 16
 17     public JarUtil(String jarPath) {
 18         this.jarPath = jarPath;
 19     }
 20
 21     //下载任务完成后,回调接口内的方法
 22     public interface OnDownloadCompleteListener {
 23         public void onSuccess(String jarPath);
 24         public void onFail();
 25     }
 26
 27     //jar不存在则返回false
 28     //若文件大小为0表示jar无效,删除该文件再返回false
 29     public boolean isJarExists() {
 30         File jar = new File(jarPath);
 31         if (!jar.exists()) {
 32             return false;
 33         }
 34         if (jar.length()==0) {
 35             jar.delete();
 36             return false;
 37         }
 38         return true;
 39     }
 40
 41     public boolean create() {
 42         try {
 43             File file = new File(jarPath);
 44             file.getParentFile().mkdirs();
 45             file.createNewFile();
 46             return true;
 47         } catch (IOException e) {
 48             return false;
 49         }
 50     }
 51
 52     public boolean delete() {
 53         File file = new File(jarPath);
 54         return file.delete();
 55     }
 56
 57     public void download(String remotePath, OnDownloadCompleteListener listener) {
 58         mListener = listener;
 59         //启动异步类发送下载请求
 60         AsyncTask<String,String,String> task = new AsyncTask<String,String,String>() {
 61             @Override
 62             protected String doInBackground(String... path) {
 63                 if (execDownload(path[0], path[1])) {
 64                     return path[1];    //成功返回jarPath
 65                 } else {
 66                     return null;    //不成功时返回null
 67                 }
 68             }
 69
 70             @Override
 71             protected void onPostExecute(String jarPath) {
 72                 if (mListener==null) return;
 73                 //根据下载任务执行结果回调
 74                 if (jarPath==null) {
 75                     mListener.onFail();
 76                 } else {
 77                     mListener.onSuccess(jarPath);
 78                 }
 79             }
 80         };
 81         task.execute(remotePath, jarPath);
 82     }
 83
 84     private boolean execDownload(String remotePath, String jarPath) {
 85         try {
 86             URLConnection connection = new URL(remotePath).openConnection();
 87             InputStream in = connection.getInputStream();
 88             byte[] bs = new byte[1024];
 89             int len = 0;
 90             OutputStream out = new FileOutputStream(jarPath);
 91             while ((len=in.read(bs))!=-1) {
 92                 out.write(bs, 0, len);
 93             }
 94             out.close();
 95             in.close();
 96             return true;
 97         } catch (IOException e) {
 98             return false;
 99         }
100     }
101 }

以下组装ClassLoader辅助类

 1 package com.kavmors.remoteloader;
 2
 3 import com.kavmors.core.RemoteEntry;
 4
 5 import android.app.Activity;
 6 import dalvik.system.DexClassLoader;
 7
 8 public class ClassLoaderUtil {
 9     private Activity mActivity;
10
11     public ClassLoaderUtil(Activity activity) {
12         mActivity = activity;
13     }
14
15     /**
16      * 读取一个类,并返回实例
17      * @param jarPath jar包的本地路径
18      * @param classPath 类所在的地址(包括package名)
19      * @return 继承RemoteEntry接口的实体类实例,失败则返回null
20      */
21     public RemoteEntry load(String jarPath, String classPath) {
22         DexClassLoader loader;
23         try {
24             String optimizedDir = mActivity.getDir(mActivity.getString(R.string.app_name), Activity.MODE_PRIVATE).getAbsolutePath();
25             loader = new DexClassLoader(jarPath, optimizedDir, null, mActivity.getClassLoader());
26             Class<?> c = loader.loadClass(classPath);
27             RemoteEntry instance = (RemoteEntry)c.newInstance();
28             return instance;
29         } catch (Exception e) {
30             return null;
31         }
32     }
33 }

简单解释DexClassLoader构造方法[3]。第一个参数dexPath表示jar文件的路径,用File.pathSeparator隔开;第二个参数是优化后dex文件的存储路径,可以理解为解压jar得到的文件的路径;第三个参数是目标类使用的本地C/C++库,这里为null;第四个参数是要加载的类的父加载器,一般是当前的加载器。需要说明,第二个参数需要宿主程序目录,只允许当前程序访问,因此不能为SD卡路径,官网上建议使用context.getCodeCacheDir().getAbsolutePath()的方法获取,在低于API 21的应用可以用上面例子的方法。为了避免漏洞,建议jar路径(第一个参数)也设为宿主目录,但由于测试中方便删除,这里将直接使用SD卡路径。

返回的RemoteEntry类很简单,传入参数为Activity

1 package com.kavmors.core;
2
3 import android.app.Activity;
4
5 public interface RemoteEntry {
6     public void execute(Activity activity);
7 }

下面开始主程序。首先生成一个布局文件activity_main.xml,内容很简单,一个TextView一个Button,分别加@+id/txt和@+id/btn。Activity的执行逻辑是,先判断jar文件是否存在,存在则直接执行类加载任务。若不存在,则下载jar到SD卡路径中,再加载。加载完成后,执行RemoteEntry.execute(Activity)。细节方面,在下载jar时生成一个ProgressDialog提示。

 1 package com.kavmors.remoteloader;
 2
 3 import java.io.File;
 4
 5 import com.kavmors.core.RemoteEntry;
 6
 7 import android.app.Activity;
 8 import android.app.ProgressDialog;
 9 import android.os.Bundle;
10 import android.os.Environment;
11 import android.widget.Toast;
12
13 public class MainActivity extends Activity implements JarUtil.OnDownloadCompleteListener {
14     private final String REMOTE_PATH = "http://127.0.0.1/kavmors/MainEntry.jar";    //服务器上MainEntry.jar的URL
15     private ProgressDialog dialog;
16
17     @Override
18     protected void onCreate(Bundle savedInstanceState) {
19         super.onCreate(savedInstanceState);
20         setContentView(R.layout.activity_main);
21
22         JarUtil util = new JarUtil(getJarPath());
23         if (util.isJarExists()) {
24             onSuccess(getJarPath());    //存在则直接执行类加载
25         } else {
26             //创建新的jar文件
27             util.create();
28             //显示ProgressDialog
29             dialog = new ProgressDialog(this);
30             dialog.setTitle("提示");
31             dialog.setMessage("加载中...");
32             dialog.show();
33             //执行下载
34             util.download(REMOTE_PATH, this);
35         }
36     }
37
38     @Override
39     public void onSuccess(String jarPath) {
40         if (dialog!=null) dialog.dismiss();
41         //使用加载器加载,获取一个RemoteEntry实例
42         RemoteEntry entry = new ClassLoaderUtil(this).load(jarPath, getClassPath());
43         if (entry==null) onFail();
44         else entry.execute(this);
45     }
46
47     @Override
48     public void onFail() {
49         if (dialog!=null) dialog.dismiss();
50         Toast.makeText(this, "Fail to load class", Toast.LENGTH_SHORT).show();
51     }
52
53     //返回jar路径
54     private String getJarPath() {
55         String exterPath = Environment.getExternalStorageDirectory().getAbsolutePath();
56         return exterPath + File.separator + this.getResources().getString(R.string.app_name) + File.separator + "MainEntry.jar";
57     }
58
59     //返回包+类路径
60     private String getClassPath() {
61         return "com.kavmors.core.MainEntry";
62     }
63 }

编译一下,这个应用框架已经完成了,先安装到机子上,但由于没有MainEntry.jar,这时运行会提示“Fail to load class.”。

3. 动态类的编译和打包

还差一个MainEntry.jar。现在创建一个MainEntry类继承RemoteEntry接口,做一些简单的控件操作。

 1 package com.kavmors.core;
 2
 3 import com.kavmors.remoteloader.R;
 4
 5 import android.app.Activity;
 6 import android.view.View;
 7 import android.widget.Button;
 8 import android.widget.TextView;
 9
10 public class MainEntry implements RemoteEntry {
11     @Override
12     public void execute(Activity activity) {
13         //控件操作
14         final TextView txt = (TextView) activity.findViewById(R.id.txt);
15         Button btn = (Button) activity.findViewById(R.id.btn);
16         btn.setOnClickListener(new View.OnClickListener() {
17             @Override
18             public void onClick(View v) {
19                 txt.setText("Button on click");
20             }
21         });
22     }
23 }

和Java应用的例子一样,把MainEntry单独打包成MainEntry.jar。这里还有一步,由于Dalvik执行dex文件,还需要把jar使用SDK包中的工具制成dex文件[4]。这个工具在SDK包中,路径为SDK/build-tools/22.0.1/dx.bat,中间的22.0.1表示API版本。可以把这个路径加入环境变量,调用命令为
【dx --dex --output=MainEntry.jar MainEntry.jar】
--output的参数表示压缩为dex后生成的文件,与原始jar同名即覆盖。压缩后,把MainEntry.jar放上服务器,服务器路径在MainActivity中定义了。

4. 总结

原理很简单,与Java加载的例子一样道理,只是ClassLoader换成了DexClassLoader,以及生成jar后要再次压缩成dex。本例只是提供一种思路,以及简述实现该思路的方法,如果要用在实际应用中,需要考虑的情况很多,如根据版本号更新jar,下载jar失败时的策略,等。应用庞大的时候需要考虑到下载更新一次jar需要很长时间,这时可以拆分为多个jar,按需更新。同时,这种方式加载可能增加被破解的风险,也带来应用签名的问题。实际情况实际考虑,有兴趣深入研究,推荐查阅【安卓插件化】的相关资料和开源框架[5]

参考资料及引用

[1] ClassLoader原理:开源中国. Java Classloader机制解析. 
http://my.oschina.net/aminqiao/blog/262601#OSC_h1_1

[2] 安卓类加载器:CSDN博客. Android中的类装载器DexClassLoader.
http://blog.csdn.net/com360/article/details/14125683

[3] DexClassLoader构造方法:Android Developers. DexClassLoader. 
http://developer.android.com/reference/dalvik/system/DexClassLoader.html

[4] dex文件:CSDN博客. class文件和dex文件的区别(DVM和JVM的区别)及Android DVM介绍. 
http://m.blog.csdn.net/blog/fangchao3652/42246049

[5] 插件化框架:Github. dynamic-load-apk. 
https://github.com/singwhatiwanna/dynamic-load-apk

转载于:https://www.cnblogs.com/kavmors/p/4761460.html

Android 动态类加载实现免安装更新相关推荐

  1. 街霸5 android,MD街头霸王5免安装版

    MD街头霸王5免安装版是一款玩法十分经典的格斗游戏,这款游戏里面有着组队战模式.画廊模式.格斗模式等多种模式供你挑战,角色丰富,画面精美逼真,技能炫酷,格斗体验极致畅爽,喜欢的朋友快来下载吧! 游戏介 ...

  2. 准备你的应用(Android免安装应用)

    最好的免安装应用体验专注于帮助用户快速完成任务(例如观看视频或进行购买).您可以开始使用此应用程序列表来准备Android免安装应用.这里的许多考虑被认为是Android应用程序的最佳做法. 从你的应 ...

  3. Android Studio 免安装版本

    免安装的程序我放到网盘了.地址 http://pan.baidu.com/s/1mgNvbHA#path=%252FAndroid%252FAndroidStudio%25E5%25B7%25A5%2 ...

  4. android 实现表格横向混动_Flutter混合开发和Android动态更新实践

    Flutter混合开发和Android动态更新实践 感谢闲鱼和csdn的文章给的思路: 本篇是实践性文章包含两部分 将Flutter工程编译后的文件集成到Android项目 将Flutter代码热更新 ...

  5. 解决VS2015安装Android SDK 后文件不全及更新问题

    解决VS2015安装Android SDK 后文件不全及更新问题 参考文章: (1)解决VS2015安装Android SDK 后文件不全及更新问题 (2)https://www.cnblogs.co ...

  6. 【Google Play】从 Android 应用中跳转到 Google Play 中 ( 跳转代码示例 | Google Play 页面的链接格式 | Google Play 免安装体验 )

    文章目录 前言 一.从 Android 应用跳转到 Google Play 代码 二.Google Play 页面的链接格式 三.Google Play 免安装体验 前言 本博客参考资料 链接到 Go ...

  7. Android应用开发提高系列(4)——Android动态加载(上)——加载未安装APK中的类...

    前言 近期做换肤功能,由于换肤程度较高,受限于平台本身,实现起来较复杂,暂时搁置了该功能,但也积累了一些经验,将分两篇文章来写这部分的内容,欢迎交流! 关键字:Android动态加载 声明 欢迎转载, ...

  8. [转]Android应用开发提高系列(4)——Android动态加载(上)——加载未安装APK中的类...

    本文转自:http://www.cnblogs.com/over140/archive/2012/03/29/2423116.html 前言 近期做换肤功能,由于换肤程度较高,受限于平台本身,实现起来 ...

  9. Android应用开发提高系列(5)——Android动态加载(下)——加载已安装APK中的类和资源...

    前言  Android动态加载(下)--加载已安装APK中的类和资源. 声明 欢迎转载,但请保留文章原始出处:)  博客园:http://www.cnblogs.com 农民伯伯: http://ov ...

最新文章

  1. java实现七日股票问题_七日打卡--JAVA资源限制
  2. yolov4网络结构_重磅更新!YoLov4最新论文!解读YoLov4框架!
  3. 学习笔记---好文章链接帖
  4. 经典C语言程序100例之十二
  5. Linux platform驱动模型
  6. 产业链布局优势明显,三星开启全新移动智能体验新时代
  7. FreeBSD学习笔记15-FreeBSD下安装Apache
  8. android obd编程,Android蓝牙连接汽车OBD设备
  9. php jq表格,如何用jQuery操作表单和表格
  10. 查询出两个表中不同的数据
  11. Makefile 入门教程
  12. WinEdt LaTex(四)—— 自定义新命令(newcommand、def)
  13. python私有化方法_Python 私有化
  14. 设计物联网系统的步骤和原则有哪些
  15. jq-ui-multiselect插件的使用
  16. easyui 如何添加事件
  17. 时间紧、任务重、资源有限,项目经理如何来保证研发效率?
  18. 国产接口芯片兼容替换TI MM1192,用于通信设备协议
  19. vue项目该不该使用eslint验证?
  20. 餐讯头条丨亚洲美食文化论坛在京圆满落幕

热门文章

  1. 为什么不敢和别人竞争_看懂这个自我评价发展曲线,你就明白,为什么青春期孩子如此叛逆...
  2. python客户价值分析_[Python数据挖掘]第7章、航空公司客户价值分析
  3. 光纤布拉格光栅matlab,matlab对各种光纤光栅的仿真
  4. linux dns chroot,系统运维|在 CentOS7.0 上搭建 Chroot 的 Bind DNS 服务器
  5. 【sprinb-boot】资源、配置、lib分离打包
  6. html filter 作用,css滤镜有什么作用?
  7. css margin和border,Margin、Border、Padding属性的区别和联系
  8. c语言一维数组课件,第9章:c语言一维数组课件
  9. python文本处理第三方库是什么_python第三方库网站
  10. 资源调度框架YARN