文档章节

动态加载APK原理分享

alu
 alu
发布于 2015/01/16 14:03
字数 2212
阅读 393
收藏 6

(一) 综述

    随着智能手机硬件性能的逐步提升,移动应用也做的越来越复杂,android平台上应用的apk包体积也越来越大,然后同类产品开始比拼谁的体积小,实现方案呢,然后很容易想到"插件化",也就是说可以发布内核很小的产品,随着添加功能的需求而动态下载功能模块,促使插件化的另一个动机是App应用固有的问题,那就是很多组件需要注册,更新功能的话不能像Web应用那样可在用户无察觉的情况下通过升级服务器而方便升级,只能弹出个框让用户重新下载整个程序包,然后调取系统安装流程。
 
    被加载的apk称之为插件,因为机制类似于生物学的"寄生",加载了插件的应用也被称为宿主。
往往不是所有的apk都可作为插件被加载,往往需要遵循一定的"开发规范",还需要插件项目引入某种api类库,业界通常都是这么做的。
这里介绍一种无须规范限制的动态加载解决方案,插件不需要依赖任何API,这也是本人突发异想,灵感所致。
项目地址:https://github.com/houkx/android-pluginmgr/欢迎star andfork

(二)功能介绍

      特点:

  •      插件为普通apk,无须依赖任何jar

  •     Activity生命周期由系统自己管理

  •     使用简单,只需要了解一个类PluginManager的两个方法

  •     启动Activity的效率高

  •     不修改插件,被加载的插件仍然可以独立安装。

     功能点:

  1. 可加载任意apk中的 Activity (包括子类 ActionBarActivity 、FragmentActivity)的派生类(不包括违反限制条件的Activity)

  2. 支持插件自定义Application

  3. 支持插件Apk中的Activity跳转到别的Activity(插件内部的或系统的,外部已安装apk的,甚至是别的插件中的),也没有任何限制

  4. 支持Activity设置主题(与系统的主题应用规则一样,如果Activity没指定Theme,但所在Application指定了Theme,则使用Application的Theme)

  5. 初步支持.so

  6. 支持插件使用 SharedPreference 或 SQLite数据库(尚未完善)

 



将要支持的特性:
        PackageManager service 等等,详情都列在开源项目android-pluginmgr
  https://github.com/houkx/android-pluginmgr/tree/experiment/android-pluginmgr下的TODO文件
限制条件(永不支持的):

  •   插件apk中不能假定自己已经安装,以及由此造成的影响,比如认为applicationInfo.dataDir==/data/data/packageName

  •  不能依赖清单文件中的进程声明,被加载的apk以及里面的任何组件目前都在同一个进程管理。

  •   插件中的权限,无法动态注册,插件中的权限都得在宿主中注册(暂无解决方案)


(三) 实现

    动态加载需要处理很多问题,虽然有很多问题,但是核心问题就是加载Activity,因为Activity是可见的,人们对可以看到的东西总是那么重视,视觉信息占人所处理信息的90%以上。
Activity如何调起来?资源的加载等等已经有大牛的文章介绍汗牛充栋了,我本菜鸟,不再赘述。 
目前Activity的加载或许有很多处理方式,但是可以分为两种:一是自己new 二是系统new 。很多动态加载框架基于第一种方式。我这个方案基于第二种
,既然要系统new,就要系统自己可以找到相应的Activity. 由于Activity需要在清单文件注册了才能使用,所以要注册Activity,但是如何注册呢?
我在网上看到有人用极端的方式:插件里的所有Activiy都在宿主里注册,既然宿主总要修改升级,何必要插件呢,这已经违背了动态加载的初衷:不修改框架而动态扩展功能更多的是这么做,注册一个Activity基类,供插件中的Activity继承,在这个基类里做动态加载的核心逻辑,这就要求插件必须依赖某种API类库。
我的方案通俗的说是这样,依赖倒转,不让插件依赖框架API,而是反过来,自动生成一个Activity类依赖(继承)插件中的Activity,这个自动生成的类就叫PluginActivity
并且声明在框架的清单文件中,如下: 
<activity name="androidx.pluginmgr.PluginActivity" /> 
聪明的读者会想,等一下,插件里面Activity可不止一个,你就注册一个?
是的,就一个,自动生成的Activity类名都是androidx.pluginmgr.PluginActivity,不过放在不同的文件中,最简单的映射,原始Activity类名.dex文件中存储对应的子类:PluginActivity
其实也是偷梁换柱了,如果你想启动插件里的Activity,如com.test.MyPlugActivity, 我就把启动目标修改为androidx.pluginmgr.PluginActivity类,
然后从com.test.MyPlugActivity.dex文件中找到 public class androidx.pluginmgr.PluginActivity extends com.test.MyPlugActivity{....}
以启动SthActivity为例:




好了,核心思想已经表达清楚了,下面介绍如何让系统按你说的路径去找类文件,这涉及到类加载器。自定义类加载器比较简单,继承java.lang.ClassLoader即可.
在我的开源项目源码中对应的类是 FrameworkClassLoader, PluginManager初始化时就去修改Application的类加载器,替换为 FrameworkClassLoader.
FrameworkClassLoader 其实不干什么实际加载工作,只是分发任务:
            public Class loadClass(String className){
  if(当前上下文插件不为空) {
      if( className 是 PluginActivity){
          找到当前实际要加载的原始 Activity
         return  使用插件对应的 ActivityClassLoader 从 (自动生成的)原始Activity类名.dex 文件 加载PluginActivity
      }else{
         return  使用对应的 PluginClassLoader 加载普通类
      }  
   }else{
         return super.loadClass()//即委派给宿主Application的原始类加载器加载
   }   
}
  其中, PluginClassLoader 是一个DexClassLoader, parent 指向 FrameworkClassLoader,
  ActivityClassLoader 也是一个DexClassLoader, parent 指向 PluginClassLoader

  

  插图2(类加载器结构图):

  

package androidx.pluginmgr;


import android.util.Log;


/**
 * 框架类加载器(Application 的 classLoder被替换成此类的实例)
 * 
 *
 */
class FrameworkClassLoader extends ClassLoader {
	private String[] plugIdAndActname;//代表插件上下文


	public FrameworkClassLoader(ClassLoader parent) {
		super(parent);
	}
    //在外部或插件内部的 startActivity 时调用这个方法设置插件上下文
	String newActivityClassName(String plugId, String actName) {
		plugIdAndActname = new String[] { plugId, actName };
		return ActivityOverider.targetClassName;//targetClassName即宿主manifest配置的androidx.pluginmgr.PluginActivity
	}


	protected Class<?> loadClass(String className, boolean resolv)
			throws ClassNotFoundException {
		Log.i("cl", "loadClass: " + className);
		String[] plugIdAndActname = this.plugIdAndActname;
		Log.i("cl", "plugIdAndActname = " + java.util.Arrays.toString(plugIdAndActname));
		if (plugIdAndActname != null) {
			String pluginId = plugIdAndActname[0];
			
			PlugInfo plugin = PluginManager.getInstance().getPluginById(
					pluginId);
			Log.i("cl", "plugin = " + plugin);
			if (plugin != null) {
				try {
					if (className.equals(ActivityOverider.targetClassName)) {
						String actClassName = plugIdAndActname[1];//被(继承)代理的Activity类名
						return plugin.getClassLoader().loadActivityClass(
								actClassName);// 加载动态生成的Activity类
					}else{
						return plugin.getClassLoader().loadClass(className);//加载普通类
					}
				} catch (ClassNotFoundException e) {
					e.printStackTrace();
				}
			}
		}
		
		return super.loadClass(className, resolv);
	}
}



到此为止,一个动态加载框架的核心雏形已经有了,但是还有许多细节待完善。

------------------------------------------------------------------------------------------------------------------------

附加:另外,关于动态生成类,对于davikvm环境,我所知道的工具有asmdex和dexmaker,我在项目中选用的是dexmaker

用什么不是重点,看生成的代码吧:(ActivityOverider 类负责与自动生成的Activity类交互)

package androidx.pluginmgr;

import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Bundle;
import androidplugdemo.SthActivity;

public final class PluginActivity
  extends SthActivity
{
  private static final String _pluginId = "activityTest_v1";
  private AssetManager mAssertManager;
  private Resources mResources;
  
  public boolean bindService(Intent paramIntent, ServiceConnection paramServiceConnection, int paramInt)
  {
    return ActivityOverider.overrideBindService(this, _pluginId, paramIntent, paramServiceConnection, paramInt);
  }
  
  public AssetManager getAssets()
  {
    AssetManager localAssetManager = this.mAssertManager;
    if (localAssetManager == null) {
      localAssetManager = super.getAssets();
    }
    return localAssetManager;
  }
  
  public Resources getResources()
  {
    Resources localResources = this.mResources;
    if (localResources == null) {
      localResources = super.getResources();
    }
    return localResources;
  }
  
  public void onBackPressed()
  {
    if (ActivityOverider.overrideOnbackPressed(this, _pluginId)) {
      super.onBackPressed();
    }
  }
  
  protected void onCreate(Bundle paramBundle)
  {
    String str = _pluginId;
    AssetManager localAssetManager = ActivityOverider.getAssetManager(str, this);
    this.mAssertManager = localAssetManager;
    Resources localResources = super.getResources();
    this.mResources = new Resources(localAssetManager, localResources.getDisplayMetrics(), localResources.getConfiguration());
    ActivityOverider.callback_onCreate(str, this);
    super.onCreate(paramBundle);
  }
  
  protected void onDestroy()
  {
    String str = _pluginId;
    super.onDestroy();
    ActivityOverider.callback_onDestroy(str, this);
  }
  
  protected void onPause()
  {
    String str = _pluginId;
    super.onPause();
    ActivityOverider.callback_onPause(str, this);
  }
  
  protected void onRestart()
  {
    String str = _pluginId;
    super.onRestart();
    ActivityOverider.callback_onRestart(str, this);
  }
  
  protected void onResume()
  {
    String str = _pluginId;
    super.onResume();
    ActivityOverider.callback_onResume(str, this);
  }
  
  protected void onStart()
  {
    String str = _pluginId;
    super.onStart();
    ActivityOverider.callback_onStart(str, this);
  }
  
  protected void onStop()
  {
    String str = _pluginId;
    super.onStop();
    ActivityOverider.callback_onStop(str, this);
  }
  
  public void startActivityForResult(Intent paramIntent, int paramInt, Bundle paramBundle)
  {
    super.startActivityForResult(ActivityOverider.overrideStartActivityForResult(this, _pluginId, paramIntent, paramInt, paramBundle), paramInt, paramBundle);
  }
  
  public ComponentName startService(Intent paramIntent)
  {
    return ActivityOverider.overrideStartService(this, _pluginId, paramIntent);
  }
  
  public boolean stopService(Intent paramIntent)
  {
    return ActivityOverider.overrideStopService(this, _pluginId, paramIntent);
  }
  
  public void unbindService(ServiceConnection paramServiceConnection)
  {
    ActivityOverider.overrideUnbindService(this, _pluginId, paramServiceConnection);
  }
}


(ps:

第一个插件代码来自 https://github.com/viacheslavtitov/NDKBegining

是个老外,不过作者也比较粗心,要正常运行你需要在sd卡下创建目录:FFMPEG

第二个插件代码来自这篇博文这篇博文

第三个插件代码来自大名鼎鼎的 Pulltorefresh

第6第7个插件代码是自测项目,分别测试ActionBarActivity和Activity基本加载和跳转

其他apk还没能正常加载,框架还在不断完善中,不过腾讯的开心消消乐可以帮助记录crashlog,这点真不错~


© 著作权归作者所有

alu

alu

粉丝 4
博文 1
码字总数 2212
作品 1
昌平
程序员
私信 提问
加载中

评论(5)

alu
alu 博主

引用来自“曹锋11”的评论

牛人阿,问一下,为什么我动态加载一些网上下的APK,如蜻蜓FM,WIFI破解师,QQ等,会出现一直黑屏的情况呢
不是万能的 有些api实现是矛盾的 有些没实现
曹锋11
牛人阿,问一下,为什么我动态加载一些网上下的APK,如蜻蜓FM,WIFI破解师,QQ等,会出现一直黑屏的情况呢
小小编辑
小小编辑
借鉴的博客地址可以统一写在最后,这样整洁一点:)
懒羊羊和灰太狼的故事
懒羊羊和灰太狼的故事
不错!mark
小小编辑
小小编辑
赞,代码能高亮就好了
Android 插件化 动态升级

最新内容请见原文:Android 插件化 动态升级 不少朋友私信以及 Android开源交流几个 QQ 群 中都问到这个问题,这里简单介绍下 1、作用 大多数朋友开始接触这个问题是因为 App 爆棚了,方法数...

Trinea
2014/09/19
3.2K
1
加载assets下APK文件 及快速加载插件APK里面的资源

Android 系统为每个新设计的程序提供了/assets目录,这个目录保存的文件可以打包在程序里。/res 和/assets的不同点是,android不为/assets下的文件生成ID。如果使用/assets下的文件,需要指定...

desaco
01/09
0
0
Android动态加载技术 系列索引

Android Dynamical Loading 大家新年好,最近花了点时间,慢慢把这个系列的内容稍微调整了下。 Last Edit: 2016-2-10 基本信息 Author:Kaedea GitHub:android-dynamical-loading 动态加载介...

Kaede
2017/11/29
0
0
Android 插件化技术窥探

在Android 插件化技术中(宿主app和插件app设置相同的sharedUserId),动态加载apk有两种方式: 一种是将资源主题包的apk安装到手机上再读取apk内的资源,这种方式的原理是将宿主app和插件a...

安卓笔记侠
02/19
0
0
Android ClassLoader浅析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zly921112/article/details/83417774 前言 最近在看Tinker的原理,发现核心是通过做的,由于之前也从未接触过...

zhuliyuan丶
2018/10/26
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Spring使用ThreadPoolTaskExecutor自定义线程池及实现异步调用

多线程一直是工作或面试过程中的高频知识点,今天给大家分享一下使用 ThreadPoolTaskExecutor 来自定义线程池和实现异步调用多线程。 一、ThreadPoolTaskExecutor 本文采用 Executors 的工厂...

CREATE_17
今天
5
0
CSS盒子模型

CSS盒子模型 组成: content --> padding --> border --> margin 像现实生活中的快递: 物品 --> 填充物 --> 包装盒 --> 盒子与盒子之间的间距 content :width、height组成的 内容区域 padd......

studywin
今天
7
0
修复Win10下开始菜单、设置等系统软件无法打开的问题

因为各种各样的原因导致系统文件丢失、损坏、被修改,而造成win10的开始菜单、设置等系统软件无法打开的情况,可以尝试如下方法解决 此方法只在部分情况下有效,但值得一试 用Windows键+R打开...

locbytes
昨天
8
0
jquery 添加和删除节点

本文转载于:专业的前端网站➺jquery 添加和删除节点 // 增加一个三和一节点function addPanel() { // var newPanel = $('.my-panel').clone(true) var newPanel = $(".triple-panel-con......

前端老手
昨天
8
0
一、Django基础

一、web框架分类和wsgiref模块使用介绍 web框架的本质 socket服务端 与 浏览器的通信 socket服务端功能划分: 负责与浏览器收发消息(socket通信) --> wsgiref/uWsgi/gunicorn... 根据用户访问...

ZeroBit
昨天
10
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部