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]。
?
參考資料及引用
[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 动态类加载实现免安装更新的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 值班问题:insert语句插入了两条数据
- 下一篇: Android中进程与线程