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

随着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.mfbz.cn/a/480634.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【C#】C#窗体应用修改窗体的标题和图标

修改窗体顶部的标题和图表&#xff0c;如果不修改则会使用默认的图标&#xff0c;标题默认为Form1&#xff0c;如第一张图&#xff0c;这时候如果想换成和系统有关的内容&#xff0c;如第二张图&#xff0c;可以使用下面的方法进行修改&#xff0c;修改后打开该软件任务栏显示的…

网络安全笔记-day6,NTFS安全权限

文章目录 NTFS安全权限常用文件系统文件安全权限打开文件安全属性修改文件安全权限1.取消父项继承权限2.添加用户访问权限3.修改用户权限4.验证文件权限5.总结权限 强制继承父项权限文件复制移动权限影响跨分区同分区 总结1.权限累加2.管理员最高权限2.管理员最高权限 NTFS安全…

开源数据集 nuScenes 之 3D Occupancy Prediction

数据总体结构 Nuscenes 数据结构 可以看一下我的blog如何下载完整版 mmdetection3d ├── mmdet3d ├── tools ├── configs ├── data │ ├── nuscenes │ │ ├── maps │ │ ├── samples │ │ ├── sweeps │ │ ├── lidarseg (o…

Prometheus Grafana 配置仪表板

#grafana# 其实grafana提供了丰富的Prometheus数据源的仪表板&#xff0c;基本上主流的都有&#xff0c;通过下面官方地址可查阅 Dashboards | Grafana Labs 这里举例说明&#xff0c;配置node_exporter仪表板 首先&#xff0c;在上面的网站搜索 node 可以查到蛮多的仪表板…

【使用redisson完成延迟队列的功能】使用redisson配合线程池完成异步执行功能,延迟队列和不需要延迟的队列

1. 使用redisson完成延迟队列的功能 引入依赖 spring-boot-starter-actuator是Spring Boot提供的一个用于监控和管理应用程序的模块 用于查看应用程序的健康状况、审计信息、指标和其他有用的信息。这些端点可以帮助你监控应用程序的运行状态、性能指标和健康状况。 已经有了…

【现代C++】统一初始化

现代C中的统一初始化&#xff08;Uniform Initialization&#xff09;是C11引入的一项特性&#xff0c;它提供了一种统一的语法来初始化任何类型的对象。统一初始化旨在增强代码的一致性和清晰度&#xff0c;减少传统初始化方式中的歧义。以下是统一初始化的几种用法及相应的示…

Leetcode 226. 翻转二叉树

心路历程&#xff1a; 翻转一瞬间没什么思路&#xff0c;其实就是挨个把每个结点的左右子树都翻转了。主要不要按照左右子树去思考&#xff0c;要按照结点去思考。 翻转既可以从上到下翻转&#xff08;前序遍历&#xff09;&#xff0c;也可以从下到上翻转&#xff08;后序遍历…

张桥社区组织“平安大讲堂”企业应急救护及消防主题培训

为进一步加强园区商户的平安生产意识&#xff0c;提升应急救护能力&#xff0c;在襄阳市民政局的指导和支持下&#xff0c;襄阳市和时代社会工作服务中心依托襄阳市“光明谷”社会组织助力共同缔造项目&#xff0c;联合樊城区红十字会、樊城区点爱志愿者协会在张桥社区“美世界…

基于飞凌嵌入式i.MX6ULL核心板的电梯智能物联网关方案

电梯是现代社会中不可或缺的基础性设施&#xff0c;为人们的生产生活提供了很大的便捷。我国目前正处于城镇化的快速发展阶段&#xff0c;由此带动的城市基础设施建设、楼宇建设、老破小改造等需求也让我国的电梯行业处在了一个高速增长期。截至2023年年底&#xff0c;中国电梯…

Linux:rpm部署Jenkins(1)

1.获取Jenkins安装包 我这里使用的是centos7系统&#xff0c;ip为&#xff1a;192.168.6.6 2G运存 连接外网 Jenkins需要java环境&#xff0c;java的jdk包你可以去网上下载离线包&#xff0c;或者直接去yum安装&#xff0c;我这里使用的是yum安装 再去获取Jenkins的rpm包…

练习 12 Web [极客大挑战 2019]BabySQL

本题复习&#xff1a;1.常规的万能语句SQL查询&#xff0c;union联合查询&#xff0c;Extractvalue()报错注入 extractvalue(1,concat(‘0x7e’,select(database())))%23 我一开始挨着试&#xff0c;感觉都无效 直到报错注入&#xff0c;查到了库名‘geek’ 尝试查表名&…

Pytest测试框架+allure+jenkins自动化持续集成

Pytest是python的一种单元测试框架&#xff0c;可通过pytest 目录路径来运行测试用例 可以通过断言assert来测试是否通过 1.pytest测试用例命名规范 需严格遵循此规范&#xff0c;不然使用 pytest 目录 来运行会找不到该条测试用例。 可通过这样定义main函数&#xf…

【目标检测】YOLOv9理论解读与代码分析

前言 YOLO这个系列的故事已经很完备了&#xff0c;比如一些Decoupled-Head或者Anchor-Free等大的策略改动已经在YOLOv8固定下来&#xff0c;后面已经估计只有拿一些即插即用的tricks进行小改。 mmdetection框架的作者深度眸也在知乎上对“是否会有YOLOv9”这一观点发表看法&a…

【深入理解 Spring 事务】实现CURD

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大三在校生&#xff0c;喜欢AI编程&#x1f38b; &#x1f43b;‍❄️个人主页&#x1f947;&#xff1a;落798. &#x1f43c;个人WeChat&#xff1a;hmmwx53 &#x1f54a;️系列专栏&#xff1a;&#x1f5bc;️…

Flutter动画(一)Ticker、Animate 原理

在任何系统的UI框架中&#xff0c;动画原理都是类似的&#xff0c;即&#xff1a;在一段时间内&#xff0c;快速地多次改变UI外观&#xff1b;由于人眼会产生视觉暂留&#xff0c;所以最终看到的就是一个“连续”的动画。 Flutter中对动画进行了抽象&#xff0c;主要涉及 Anim…

在Windows中安装wsl2和ubuntu22.04

目录 一、概述二、安装wsl22.1 虚拟化设置2.2 虚拟化设置2.3 切换和更新wsl2 三、安装ubuntu3.1 下载Ubuntu22.043.2 配置Ubuntu22.04 一、概述 wsl2是一种面向Windows操作系统的虚拟化技术&#xff0c;可以让我们在Windows操作系统中“丝滑”的运行Linux系统。wsl2由微软团队…

时序预测 | Matlab实现BiTCN-GRU双向时间卷积神经网络结合门控循环单元时间序列预测

时序预测 | Matlab实现BiTCN-GRU双向时间卷积神经网络结合门控循环单元时间序列预测 目录 时序预测 | Matlab实现BiTCN-GRU双向时间卷积神经网络结合门控循环单元时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现BiTCN-GRU双向时间卷积神经网络结…

Vue模块化开发步骤—遇到的问题—解决办法

目录 1.npm install webpack -g 2.npm install -g vue/cli-init 3.初始化vue项目 4.启动vue项目 Vscode初建Vue时几个需要注意的问题-CSDN博客 1.npm install webpack -g 全局安装webpack 直接命令提示符运行改指令会报错&#xff0c;operation not permitted 注意&#…

【JavaScript 漫游】【041】File 对象、FileList 对象、FileReader 对象

文章简介 本篇文章为【JavaScript 漫游】专栏的第 041 篇文章&#xff0c;主要对浏览器模型中 File 对象、FileList 对象和 FileReader 对象的知识点进行了简记。 File 对象 File 对象代表一个文件&#xff0c;用来读写文件信息。它继承了 Blob 对象&#xff0c;或者说是一种…

Microsoft Edge 中的 Internet Explorer 模式解决ie禁止跳转到edge问题

作为网工&#xff0c;网络中存在很老的设备只能用ie浏览器访问打开&#xff0c;但是win10后打开Internet Explorer 会强制跳转到Edge 浏览器&#xff0c;且有人反馈不会关&#xff0c;为此找到了微软官方的Microsoft Edge 中的 Internet Explorer 模式&#xff0c;可以直接在Mi…