登录 立即注册
安币:

查看: 27351|回复: 47

每天一个开源代码——第12个 Android apk动态加载机制的研究

[复制链接]

16

主题

79

帖子

28

安币

初级码农

Rank: 1

QQ达人QQ达人

发表于 2014-9-7 18:44:52 | 显示全部楼层 |阅读模式


1.问题:
  app内的方法数超过谷歌限制的64k,即app爆棚了;
  工程量大,编译缓慢。

2.解决方案:app的动态部署,也叫做app的插件化。

3.原理:  
  先安装一个空壳apk(也叫宿主程序),然后动态加载其他apk在宿主程序中运行。
4.问题:   
①.资源文件的读取。因为apk加载到宿主程序中去,无法通过context去读取。
解决方案:通过加载apk的资源到Resurce中来,这样就能用R文件找到资源文件了;
②.activity生命周期的管理
  加载到宿主程序的activity和一般的类一样,只能手动管理生命周期,手动传入各参数

5.详细设计:
宿主程序的实现:
①.主界面,放了一个button,点击就会调起apk,我把apk直接放在了sd卡中,至于先把apk从网上下载到本地再加载其实是一个道理。

1 [java]
2 @Override  
3 public void onClick(View v) {  
4     if (v == mOpenClient) {  
5         Intent intent = new Intent(this, ProxyActivity.class);  
6         intent.putExtra(ProxyActivity.EXTRA_DEX_PATH, "/mnt/sdcard/DynamicLoadHost/plugin.apk");  
7         startActivity(intent);  
8     }  
9   
10 }  

②.首先要加载apk中的资源:
11 protected void loadResources() {  
12     try {  
13         AssetManager assetManager = AssetManager.class.newInstance();  
14         Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);  
15         addAssetPath.invoke(assetManager, mDexPath);  
16         mAssetManager = assetManager;  
17     } catch (Exception e) {  
18         e.printStackTrace();  
19     }  
20     Resources superRes = super.getResources();  
21     mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),  
22             superRes.getConfiguration());  
23     mTheme = mResources.newTheme();  
24     mTheme.setTo(super.getTheme());  
25 }  
说明:加载的方法是通过反射,通过调用AssetManager中的addAssetPath方法,我们可以将一个apk中的资源加载到Resources中,由于addAssetPath是隐藏api我们无法直接调用,所以只能通过反射,下面是它的声明,通过注释我们可以看出,传递的路径可以是zip文件也可以是一个资源目录,而apk就是一个zip,所以直接将apk的路径传给它,资源就加载到AssetManager中了,然后再通过AssetManager来创建一个新的Resources对象,这个对象就是我们可以使用的apk中的资源了,这样我们的问题就解决了。

③.activity生命周期的管理
我们要在代理activity中去反射apkactivity的所有生命周期的方法,然后将activity的生命周期和代理activity的生命周期进行同步。首先,反射activity生命周期的所有方法,还反射了onActivityResult这个方法,尽管它不是典型的生命周期方法,但是它很有用。
26 protected void instantiateLifecircleMethods(Class<?> localClass) {  
27     String[] methodNames = new String[] {  
28             "onRestart",  
29             "onStart",  
30             "onResume",  
31             "onPause",  
32             "onStop",  
33             "onDestory"  
34     };  
35     for (String methodName : methodNames) {  
36         Method method = null;  
37         try {  
38             method = localClass.getDeclaredMethod(methodName, new Class[] { });  
39             method.setAccessible(true);  
40         } catch (NoSuchMethodException e) {  
41             e.printStackTrace();  
42         }  
43         mActivityLifecircleMethods.put(methodName, method);  
44     }  
45   
46     Method onCreate = null;  
47     try {  
48         onCreate = localClass.getDeclaredMethod("onCreate", new Class[] { Bundle.class });  
49         onCreate.setAccessible(true);  
50     } catch (NoSuchMethodException e) {  
51         e.printStackTrace();  
52     }  
53     mActivityLifecircleMethods.put("onCreate", onCreate);  
54   
55     Method onActivityResult = null;  
56     try {  
57         onActivityResult = localClass.getDeclaredMethod("onActivityResult",  
58                 new Class[] { int.class, int.class, Intent.class });  
59         onActivityResult.setAccessible(true);  
60     } catch (NoSuchMethodException e) {  
61         e.printStackTrace();  
62     }  
63     mActivityLifecircleMethods.put("onActivityResult", onActivityResult);  
64 }  
其次,同步生命周期,主要看一下onResumeonPause,其他方法是类似的。看如下代码,很好理解,就是当系统调用代理activity生命周期方法的时候,就通过反射去显式调用apkactivity的对应方法。
65 @Override  
66 protected void onResume() {  
67     super.onResume();  
68     Method onResume = mActivityLifecircleMethods.get("onResume");  
69     if (onResume != null) {  
70         try {  
71             onResume.invoke(mRemoteActivity, new Object[] { });  
72         } catch (Exception e) {  
73             e.printStackTrace();  
74         }  
75     }  
76 }  
77   
78 @Override  
79 protected void onPause() {  
80     Method onPause = mActivityLifecircleMethods.get("onPause");  
81     if (onPause != null) {  
82         try {  
83             onPause.invoke(mRemoteActivity, new Object[] { });  
84         } catch (Exception e) {  
85             e.printStackTrace();  
86         }  
87     }  
88     super.onPause();  
89 }  

待执行apk的设计:


1. 为了让proxy全面接管apk中所有activity的执行,需要为activity定义一个基类BaseActivity,在基类中处理代理相关的事情,同时BaseActivity还对是否使用代理进行了判断,如果不使用代理,那么activity的逻辑仍然按照正常的方式执行,也就是说,这个apk既可以按照执行,也可以由宿主程序来执行。
90 package com.ryg.dynamicloadclient;  
91   
92 import android.app.Activity;  
93 import android.content.Intent;  
94 import android.os.Bundle;  
95 import android.util.Log;  
96 import android.view.View;  
97 import android.view.ViewGroup.LayoutParams;  
98   
99 public class BaseActivity extends Activity {  
100   
101     private static final String TAG = "Client-BaseActivity";  
102   
103     public static final String FROM = "extra.from";  
104     public static final int FROM_EXTERNAL = 0;  
105     public static final int FROM_INTERNAL = 1;  
106     public static final String EXTRA_DEX_PATH = "extra.dex.path";  
107     public static final String EXTRA_CLASS = "extra.class";  
108   
109     public static final String PROXY_VIEW_ACTION = "com.ryg.dynamicloadhost.VIEW";  
110     public static final String DEX_PATH = "/mnt/sdcard/DynamicLoadHost/plugin.apk";  
111   
112     protected Activity mProxyActivity;  
113     protected int mFrom = FROM_INTERNAL;  
114   
115     public void setProxy(Activity proxyActivity) {  
116         Log.d(TAG, "setProxy: proxyActivity= " + proxyActivity);  
117         mProxyActivity = proxyActivity;  
118     }  
119   
120     @Override  
121     protected void onCreate(Bundle savedInstanceState) {  
122         if (savedInstanceState != null) {  
123             mFrom = savedInstanceState.getInt(FROM, FROM_INTERNAL);  
124         }  
125         if (mFrom == FROM_INTERNAL) {  
126             super.onCreate(savedInstanceState);  
127             mProxyActivity = this;  
128         }  
129         Log.d(TAG, "onCreate: from= " + mFrom);  
130     }  
131   
132     protected void startActivityByProxy(String className) {  
133         if (mProxyActivity == this) {  
134             Intent intent = new Intent();  
135             intent.setClassName(this, className);  
136             this.startActivity(intent);  
137         } else {  
138             Intent intent = new Intent(PROXY_VIEW_ACTION);  
139             intent.putExtra(EXTRA_DEX_PATH, DEX_PATH);  
140             intent.putExtra(EXTRA_CLASS, className);  
141             mProxyActivity.startActivity(intent);  
142         }  
143     }  
144   
145     @Override  
146     public void setContentView(View view) {  
147         if (mProxyActivity == this) {  
148             super.setContentView(view);  
149         } else {  
150             mProxyActivity.setContentView(view);  
151         }  
152     }  
153   
154     @Override  
155     public void setContentView(View view, LayoutParams params) {  
156         if (mProxyActivity == this) {  
157             super.setContentView(view, params);  
158         } else {  
159             mProxyActivity.setContentView(view, params);  
160         }  
161     }  
162   
163     @Deprecated  
164     @Override  
165     public void setContentView(int layoutResID) {  
166         if (mProxyActivity == this) {  
167             super.setContentView(layoutResID);  
168         } else {  
169             mProxyActivity.setContentView(layoutResID);  
170         }  
171     }  
172   
173     @Override  
174     public void addContentView(View view, LayoutParams params) {  
175         if (mProxyActivity == this) {  
176             super.addContentView(view, params);  
177         } else {  
178             mProxyActivity.addContentView(view, params);  
179         }  
180     }  
181 }  
说明:相信大家一看代码就明白了,其中setProxy方法的作用就是为了让宿主程序能够接管自己的执行,一旦被接管以后,其所有的执行均通过proxy,且Context也变成了宿主程序的Context,也许这么说比较形象:宿主程序其实就是个空壳,它只是把其它apk加载到自己的内部去执行,这也就更能理解为什么资源访问变得很困难,你会发现好像访问不到apk中的资源了,的确是这样的,但是目前我还没有很好的方法去解决。
2. 入口activity的实现
182 public class MainActivity extends BaseActivity {  
183   
184     private static final String TAG = "Client-MainActivity";  
185   
186     @Override  
187     protected void onCreate(Bundle savedInstanceState) {  
188         super.onCreate(savedInstanceState);  
189         initView(savedInstanceState);  
190     }  
191   
192     private void initView(Bundle savedInstanceState) {  
193         mProxyActivity.setContentView(generateContentView(mProxyActivity));  
194     }  
195   
196     private View generateContentView(final Context context) {  
197         LinearLayout layout = new LinearLayout(context);  
198         layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,  
199                 LayoutParams.MATCH_PARENT));  
200         layout.setBackgroundColor(Color.parseColor("#F79AB5"));  
201         Button button = new Button(context);  
202         button.setText("button");  
203         layout.addView(button, LayoutParams.MATCH_PARENT,  
204                 LayoutParams.WRAP_CONTENT);  
205         button.setOnClickListener(new OnClickListener() {  
206             @Override  
207             public void onClick(View v) {  
208                 Toast.makeText(context, "you clicked button",  
209                         Toast.LENGTH_SHORT).show();  
210                 startActivityByProxy("com.ryg.dynamicloadclient.TestActivity");  
211             }  
212         });  
213         return layout;  
214     }  
215   
216 }  
说明:由于访问不到apk中的资源了,所以界面是代码写的,而不是写在xml中,因为xml读不到了,这也是个大问题。注意到主界面中有一个button,点击后跳到了另一个activity,这个时候是不能直接调用系统的startActivity方法的,而是必须通过宿主程序中的proxy来执行,原因很简单,首先apk本书没有Context,所以它无法调起activity,另外由于这个子activityapk中的,通过宿主程序直接调用它也是不行的,因为它对宿主程序来说是不可见的,所以只能通过proxy来调用,是不是感觉很麻烦?但是,你还有更好的办法吗?
3. activity的实现
217 package com.ryg.dynamicloadclient;  
218   
219 import android.graphics.Color;  
220 import android.os.Bundle;  
221 import android.view.ViewGroup.LayoutParams;  
222 import android.widget.Button;  
223   
224 public class TestActivity extends BaseActivity{  
225   
226     @Override  
227     protected void onCreate(Bundle savedInstanceState) {  
228         super.onCreate(savedInstanceState);  
229         Button button = new Button(mProxyActivity);  
230         button.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,  
231                 LayoutParams.MATCH_PARENT));  
232         button.setBackgroundColor(Color.YELLOW);  
233         button.setText("这是测试页面");  
234         setContentView(button);  
235     }  
236   
237 }  
说明:代码很简单,不用介绍了,同理,界面还是用代码来写的。


回复即可下载源码!
游客,如果您要查看本帖隐藏内容请回复

3

主题

49

帖子

0

安币

初级码农

Rank: 1

QQ达人

发表于 2014-9-25 15:31:00 | 显示全部楼层
没看懂,感觉好高升的样子,能否解释一下

0

主题

17

帖子

5

安币

初级码农

Rank: 1

发表于 2014-10-9 09:29:18 | 显示全部楼层
参考一下,参考一下,参考一下,参考一下,参考一下,

10

主题

168

帖子

256

安币

攻城狮

菜鸟

Rank: 3Rank: 3

QQ
发表于 2014-10-9 09:41:30 | 显示全部楼层
学习一下。。。。。跟我之前写的有点不一样

10

主题

168

帖子

256

安币

攻城狮

菜鸟

Rank: 3Rank: 3

QQ
发表于 2014-10-9 09:41:47 | 显示全部楼层
{:6_154:}{:6_155:}{:6_153:}{:6_160:}

21

主题

190

帖子

462

安币

攻城狮

Rank: 3Rank: 3

发表于 2014-10-15 19:16:24 | 显示全部楼层
可以用apkplug框架啊,很完善的动态加载框架也是免费的www.apkplug.com

0

主题

31

帖子

15

安币

初级码农

Rank: 1

QQ达人

发表于 2014-10-30 23:00:24 | 显示全部楼层
支持
一下。。

0

主题

152

帖子

123

安币

程序猿

Rank: 2

QQ达人

发表于 2014-11-6 10:21:19 | 显示全部楼层
{:6_160:}{:6_160:}{:6_160:}{:6_160:}{:6_160:}
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

站长推荐

通过邮件订阅最新安卓weekly信息
上一条 /4 下一条

下载安卓巴士客户端

全国最大的安卓开发者社区
联系我们
关闭
合作电话:
13802416937
Email:
435399051@qq.com
商务市场合作/投稿
问题反馈及帮助
联系我们

广告投放| 下载客户端|申请友链|手机版|站点统计|安卓巴士 ( 粤ICP备15117877号 )

快速回复 返回顶部 返回列表