Android 动态类加载实现免安装更新

news2024/12/28 4:19:08

随着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]

Android 动态类加载实现免安装更新 - KavMors - 博客园 (cnblogs.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1538333.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

宜搭低代码高级认证实操题2 faas连接器加密解密

密钥维护页-保证有一条数据 敏感信息提交页 存档页&#xff0c;只是用来存数据的审批的时候不用这个表提交数据不然会出两条 授权查看页 FaaS连接器先下载好他的示例代码然后按照要求配置好参数直接拷贝进去就行 然后需要在云开发环境里面先new一个terminal然后跑一下./builde…

全智能深度演进,一键成片让视频创作颠覆式提效

全智能一键成片&#xff0c;让内容创作的「边际成本」逼近于零。 大模型和AIGC技术的发展&#xff0c;可以用“日新月异”来形容&#xff0c;其迭代速度史无前例&#xff0c;涌现出的各类垂直应用模型&#xff0c;也使得音视频行业的应用场景更加广泛和多样化。 然而&#xff…

Mora: Enabling Generalist Video Generation via A Multi-Agent Framework

Mora: Enabling Generalist Video Generation via A Multi-Agent Framework PDF: https://arxiv.org/html/2403.13248v1 1 概述 为弥补Sora不开源的缺陷&#xff0c;本文提出多代理框架Mora&#xff0c;整合先进视觉AI代理&#xff0c;复制Sora的全能视频生成能力。Mora能利用…

目标检测——PP-YOLOE-R算法解读

PP-YOLO系列&#xff0c;均是基于百度自研PaddlePaddle深度学习框架发布的算法&#xff0c;2020年基于YOLOv3改进发布PP-YOLO&#xff0c;2021年发布PP-YOLOv2和移动端检测算法PP-PicoDet&#xff0c;2022年发布PP-YOLOE和PP-YOLOE-R。由于均是一个系列&#xff0c;所以放一起解…

网络带宽 (网速) 在线测试

网络带宽 [网速] 在线测试 1. 测网速2. SPEEDTEST3. 下载、上传4. 宽带速率对照表5. 时延6. 抖动7. 丢包8. 测速节点9. 网线References 1. 测网速 https://www.speedtest.cn/ 2. SPEEDTEST https://www.speedtest.net/ ​ 3. 下载、上传 网络数据传输分为发送数据和接收数据…

Uni-app/Vue/Js本地模糊查询,匹配所有字段includes和some方法结合使用e

天梦星服务平台 (tmxkj.top)https://tmxkj.top/#/ 1.第一步 需要一个数组数据 {"week": "全部","hOutName": null,"weekendPrice": null,"channel": "门市价","hOutId": 98,"cTime": "…

WM8978 —— 带扬声器驱动程序的立体声编解码器(5)

接前一篇文章&#xff1a;WM8978 —— 带扬声器驱动程序的立体声编解码器&#xff08;4&#xff09; 九、寄存器概览与详解 1. 整体概览 WM8978芯片共有58个寄存器&#xff0c;整体总表如下&#xff1a; 2. 详细说明 在此&#xff0c;只介绍WM8978较为常用的那些寄存器。 &…

Vue2(九):尚硅谷TodoList案例(初级版):组件化编码流程的使用

一、组件化编码流程及资料 百度网盘 请输入提取码 提取码yyds &#xff08;Vue全家桶资料&#xff09; 组件化编码流程分为三步&#xff1a; 二、实现静态组件 1、分析结构 确定组件名称&#xff08;Header,List,Item,Footer&#xff09;和个数&#xff0c;还有嵌套关系(…

InnoDB 缓存

本文主要聊InnoDB内存结构, 先来看下官网Mysql 8.0 InnoDB架构图 MySQL :: MySQL 8.0 Reference Manual :: 17.4 InnoDB Architecture 如上图所示,InnoDB内存主要包含Buffer Pool, Change Buffer, Log Buffer, Adaptive Hash Index Buffer Pool 其实 buffer pool 就是内存中的…

从0到1实现RPC | 03 重载方法和参数类型转换

一、存在的问题 1.重载方法在当前的实现中还不支持&#xff0c;调用了会报错。 2.类型转换也还存在问题。 假设定义的接口如下&#xff0c;参数是float类型。 在Provider端接受到的是一个Double类型&#xff0c;这是因为web应用接收的请求后处理的类型。 在反射调用的时候就会…

如何用java使用es

添加依赖 如何连接es客户端 RestHighLevelClient 代表是高级客户端 其中hostname&#xff1a;es的服务器地址&#xff0c;prot端口号 &#xff0c;scheme&#xff1a;http还是https 如果不在使用es可以进行关闭&#xff0c;可以防止浪费一些资源 java如何创建索引&#xff1…

银河麒麟系统V10上安装TTS语音模块,并使用C#调用进行语音播报

银河麒麟系统V10上安装TTS语音模块,并使用C#调用进行语音播报 系统版本什么是TTS需求背景环境部署更新系统安装版本包安装完成执行命令测试C# 环境下调用语音播报系统版本 什么是TTS 从文本到语音 TTS是“Text To Speech”的缩写,即“从文本到语音”,是人机对话的一部分,让…

【源头活水】顶刊解读!IEEE T-PAMI (CCF-A,IF 23.6)2024年46卷第一期 [1]

“问渠那得清如许&#xff0c;为有源头活水来”&#xff0c;通过前沿领域知识的学习&#xff0c;从其他研究领域得到启发&#xff0c;对研究问题的本质有更清晰的认识和理解&#xff0c;是自我提高的不竭源泉。为此&#xff0c;我们特别精选论文阅读笔记&#xff0c;开辟“源头…

二十一 超级数据查看器 讲解稿 设置启动密码

二十一 超级数据查看器 讲解稿 设置启动密码 ​​​点击此处 以新页面 打开B站 播放当前教学视频 点击访问app下载页面 百度手机助手 下载地址 大家好&#xff0c;这节课我们讲解超级数据查看器高级功能&#xff0c;讲解设置启动密码&#xff0c;这是高级功能第一集。 设…

集简云新增“文本语音转换”功能,实现智能语音交互

为丰富人工智能领域的应用集成&#xff0c;为用户提供更便捷和智能化的信息获取和视觉创作方式&#xff0c;本周集简云上线了内置应用—文本语音转换。目前支持OpenAI TTS和TTS HD模型&#xff0c;实现文本语音高效智能转换&#xff0c;也可根据你的产品或品牌创建独特的神经网…

ChatGPT无法登录,提示我们检测到可疑的登录行为?如何解决?

OnlyFans 订阅教程移步&#xff1a;【保姆级】2024年最新Onlyfans订阅教程 Midjourney 订阅教程移步&#xff1a; 【一看就会】五分钟完成MidJourney订阅 GPT-4.0 升级教程移步&#xff1a;五分钟开通GPT4.0 如果你需要使用Wildcard开通GPT4、Midjourney或是Onlyfans的话&am…

在服务器上训练faster-rcnn模型(pycharm和Termius)

本文主要介绍使用服务器训练模型的两种方式&#xff1a;pycharm&#xff08;可视化界面友好&#xff09;and终端&#xff0c;本人用的是macos系统&#xff0c;可能pycharm某些入口的位置不一样&#xff0c;本教程代码以faster-rcnn为例 准备工作&#xff1a; 1.知道自己所用服…

数字乡村引领新风尚:科技赋能农村实现全面进步

随着信息技术的迅猛发展&#xff0c;数字乡村正成为引领农村全面进步的新风尚。科技作为推动农村发展的强大引擎&#xff0c;正在深刻改变着传统农业的生产方式、农村的社会结构以及农民的生活方式&#xff0c;为农村经济社会的全面进步注入了新的活力和动力。本文将从数字乡村…

OpenI启智平台创建天数智芯训练任务(以AlexNet为例)

天数智芯DeepSpark代码仓地址&#xff1a; iluvatar/DeepSpark - DeepSpark - OpenI - 启智AI开源社区提供普惠算力&#xff01; (pcl.ac.cn) 首先我们进到天数智芯DeepSpark代码仓&#xff0c;点击【派生】将代码仓Fork到我们自己的目录下 Fork好代码仓后我们到我们Fork后的代…

6、kubenetes 卷

1、什么是卷 在某些场景下&#xff0c;我们可能希望新的容器可以在之前容器结束的位 置继续运⾏&#xff0c;⽐如在物理机上重启进程。可能不需要&#xff08;或者不想要&#xff09; 整个⽂件系统被持久化&#xff0c;但又希望能保存实际数据的⽬录。 Kubernetes通过定义存储…