CSDN日报20170707——《稀缺:百分之二的选择》      征文 | 你会为 AI 转型么?      每周荐书 | Android、Keras、ES6(评论送书)

[置顶] 滴滴开源Android插件化框架VirtualAPK原理分析

标签: VirtualAPKAndroid插件化滴滴代理
390人阅读 评论(0) 收藏 举报

分类:

作者同类文章X

目录(?)[+]

  1. 概述
  2. Activity 支持
    1. Hook ActivityManagerService
    2. Hook Instrumentation
    3. 启动插件Activity
  3. Service 支持
  4. ContentProvider 支持
  5. Receiver 支持
  6. 小结
  • 概述
  • Activity 支持
    • Hook ActivityManagerService
    • Hook Instrumentation
    • 启动插件Activity
  • Service 支持
  • ContentProvider 支持
  • Receiver 支持
  • 小结

概述

滴滴出行公司的首个对外开源项目 - VirtualAPK。地址:https://github.com/didi/VirtualAPK

滴滴自行研发了这款插件化框架,功能全面、兼容性好,还能够适用于有耦合的业务插件,这就是VirtualAPK存在的意义。业内认为,在加载耦合插件方面,VirtualAPK可以说是开源方案的首选。据说滴滴打车里面已经用上了,所以还是有必要一探究竟的~~

VirtualAPK 的工作流程如图所示:

VirtualAPK 对插件没有额外的约束,原生的 apk 即可作为插件。插件工程编译生成 apk 后,即可通过宿主 App 加载,每个插件 apk 被加载后,都会在宿主中创建一个单独的 LoadedPlugin 对象。如上图所示,通过这些 LoadedPlugin 对象,VirtualAPK 就可以管理插件并赋予插件新的意义,使其可以像手机中安装过的App一样运行。

Activity 支持

Hook ActivityManagerService

插件化支持首先要解决的一点就是插件里的Activity并未在宿主程序的 AndroidMainfest.xml 注册,常规方法肯定无法直接启动插件的Activity,这个时候就需要去了解Activity的启动流程,关于启动过程主要的几个步骤请参考:浅析Android Activity的启动过程

从上文中可知,Activity 启动实际上是调用了 Instrumentation.execStartActivity 这个方法。源码如下:

public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,  Intent intent, int requestCode, Bundle options) {  IApplicationThread whoThread = (IApplicationThread) contextThread;  if (mActivityMonitors != null) {  synchronized (mSync) {  final int N = mActivityMonitors.size();  for (int i=0; i<N; i++) { //先查找一遍看是否存在这个activityfinal ActivityMonitor am = mActivityMonitors.get(i);  if (am.match(who, null, intent)) {  am.mHits++;  if (am.isBlocking()) {  return requestCode >= 0 ? am.getResult() : null;  }  break;  }  }  }  }  try {  intent.migrateExtraStreamToClipData();  intent.prepareToLeaveProcess();  //这里才是真正打开activity的地方,其核心功能在whoThread中完成。int result = ActivityManagerNative.getDefault().startActivity(whoThread, who.getBasePackageName(), intent,  intent.resolveTypeIfNeeded(who.getContentResolver()),token, target != null ? target.mEmbeddedID : null,  requestCode, 0, null, options);         checkStartActivityResult(result, intent); // 处理各种异常,如ActivityNotFound} catch (RemoteException e) {  }  return null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

可见, startActivity 最终通过 ActivityManagerNative.getDefault() 远程调用了AMS的startActivity方法, ActivityManagerNative 实际上就是 ActivityManagerService 这个远程对象的 Binder 代理对象,每次需要与AMS交互时,需要通过这个 Binder 对象完成远程IPC调用。

还不了解Binder的童鞋,可以看看老罗的Android进程间通信(IPC)机制Binder简要介绍和学习计划


// ActivityManagerNative.getDefault()
static public IActivityManager getDefault() {return gDefault.get();
}private static final Singleton<iactivitymanager> gDefault = new Singleton<iactivitymanager>() {protected IActivityManager create() {IBinder b = ServiceManager.getService("activity");if (false) {Log.v("ActivityManager", "default service binder = " + b);}IActivityManager am = asInterface(b);if (false) {Log.v("ActivityManager", "default service = " + am);}return am;}
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

从这我们可以知道,ActivityManagerNative.getDefault() 实际上是返回了一个 IActivityManager 的单例对象。

那么,VirtualApk 所要做的第一件事,就是把这个 AMS 代理对象保存起来。首先,我们可以看一下 VirtualApk 核心库里面 com.didi.virtualapk.PluginManager 这个类的初始化:


// 构造方法
private PluginManager(Context context) {Context app = context.getApplicationContext();if (app == null) {this.mContext = context;} else {this.mContext = ((Application)app).getBaseContext();}prepare();
}// 初始化
private void prepare() {Systems.sHostContext = getHostContext();this.hookInstrumentationAndHandler();this.hookSystemServices();
}/*** Hook 出一个IActivityManager,也就是 AMS 的代理对象*/
private void hookSystemServices() {try {// 反射调用 ActivityManagerNative.getDefault(),实际上这在6.0中是公开的静态方法,反射可能是考虑到版本兼容性吧?Singleton<IActivityManager> defaultSingleton = (Singleton<IActivityManager>) ReflectUtil.getField(ActivityManagerNative.class, null, "gDefault");// 通过动态代理的方式去创建代理对象,之后所有ActivityManagerNative中的方法被调用的时候都会经过这个代理IActivityManager activityManagerProxy = ActivityManagerProxy.newInstance(this, defaultSingleton.get());// Hook IActivityManager from ActivityManagerNative,实际上就是把 ActivityManagerNative 替换为刚创建的 activityManagerProxyReflectUtil.setField(defaultSingleton.getClass().getSuperclass(), defaultSingleton, "mInstance", activityManagerProxy);if (defaultSingleton.get() == activityManagerProxy) {// 两者一样,保存下来this.mActivityManager = activityManagerProxy;}} catch (Exception e) {e.printStackTrace();}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

实际上除了 startActivity 是调用 AMS 的方法以外,startServicebindService 等方法,最终调用到AMS的里的方法,这个我们在动态代理类 com.didi.virtualapk.delegate 也可以找到:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if ("startService".equals(method.getName())) {try {return startService(proxy, method, args);} catch (Throwable e) {Log.e(TAG, "Start service error", e);}} else if ("stopService".equals(method.getName())) {try {return stopService(proxy, method, args);} catch (Throwable e) {Log.e(TAG, "Stop Service error", e);}} else if ("stopServiceToken".equals(method.getName())) {try {return stopServiceToken(proxy, method, args);} catch (Throwable e) {Log.e(TAG, "Stop service token error", e);}} else if ("bindService".equals(method.getName())) {try {return bindService(proxy, method, args);} catch (Throwable e) {e.printStackTrace();}} else if ("unbindService".equals(method.getName())) {try {return unbindService(proxy, method, args);} catch (Throwable e) {e.printStackTrace();}} else if ("getIntentSender".equals(method.getName())) {try {getIntentSender(method, args);} catch (Exception e) {e.printStackTrace();}} else if ("overridePendingTransition".equals(method.getName())){try {overridePendingTransition(method, args);} catch (Exception e){e.printStackTrace();}}try {// sometimes system binder has problems.return method.invoke(this.mActivityManager, args);} catch (Throwable th) {Throwable c = th.getCause();if (c != null && c instanceof DeadObjectException) {// retry connect to system binderIBinder ams = ServiceManager.getService(Context.ACTIVITY_SERVICE);if (ams != null) {IActivityManager am = ActivityManagerNative.asInterface(ams);mActivityManager = am;}}Throwable cause = th;do {if (cause instanceof RemoteException) {throw cause;}} while ((cause = cause.getCause()) != null);throw c != null ? c : th;}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

所以实际上就等同于我们重写了一些 ActivityService 的相关操作。

Hook Instrumentation

回过头去看看 Instrumentation.execStartActivity 这个方法,在最后有这么一句代码:

checkStartActivityResult(result, intent); // 处理各种异常,如ActivityNotFound
  • 1
  • 1
static void checkStartActivityResult(int res, Object intent) {  if (res >= ActivityManager.START_SUCCESS) {  return;  }  switch (res) {  case ActivityManager.START_INTENT_NOT_RESOLVED:  case ActivityManager.START_CLASS_NOT_FOUND:  if (intent instanceof Intent && ((Intent)intent).getComponent() != null)  throw new ActivityNotFoundException(  "Unable to find explicit activity class "  + ((Intent)intent).getComponent().toShortString()  + "; have you declared this activity in your AndroidManifest.xml?");  throw new ActivityNotFoundException(  "No Activity found to handle " + intent);  case ActivityManager.START_PERMISSION_DENIED:  throw new SecurityException("Not allowed to start activity "  + intent);  case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:  throw new AndroidRuntimeException(  "FORWARD_RESULT_FLAG used while also requesting a result");  case ActivityManager.START_NOT_ACTIVITY:  throw new IllegalArgumentException(  "PendingIntent is not an activity");  default:  throw new AndroidRuntimeException("Unknown error code "  + res + " when starting " + intent);  }
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

相信大家对上面的这些异常信息不陌生吧,其中最熟悉的非 Unable to find explicit activity class 莫属了,如果 Activity 没有在 AndroidMainfest.xml 注册,此异常将会抛出。

那么就得思考一个问题了,插件的 Activity 并未在宿主程序的 AndroidMainfest.xml 注册,要如何才能绕过这一层检测?

前文中提到,com.didi.virtualapk.PluginManager 这个类的初始化的时候,除了 Hook 出一个 AMS 代理对象以外,还 Hook 出一个 Instrumentation 对象。代码如下:

private void hookInstrumentationAndHandler() {try {Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext);if (baseInstrumentation.getClass().getName().contains("lbe")) {// reject executing in paralell space, for example, lbe.System.exit(0);}// 创建自定义的 instrumentation,重写了 newActivity() 等一些方法// baseInstrumentation 后面还会用到,也保存下来final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation);// 获取 ActivityThread 的实例Object activityThread = ReflectUtil.getActivityThread(this.mContext);// 用自定义的 instrumentation 替换掉 ActivityThread 里面的 instrumentationReflectUtil.setInstrumentation(activityThread, instrumentation);ReflectUtil.setHandlerCallback(this.mContext, instrumentation);this.mInstrumentation = instrumentation;} catch (Exception e) {e.printStackTrace();}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

既然 Activity 的启动,最后走了 Instrumentation.execStartActivity 这个方法,那么我们大概可以知道,Hook 出一个 Instrumentation 对象用来做什么了,实际上就是用来帮助启动插件的 Activity

启动插件Activity

我们 Hook 了一个 VAInstrumentation 已替代系统的 Instrumentation,这样当系统通过 ActivityThread 调用 它的的成员变量 mInstrumentation 的 newActivity() 等方法的时候,实际是调用我们 VAInstrumentationnewActivity()

实际上对于插件 Activity 启动,采用的是宿主 manifest 中占坑的方式来绕过系统校验,然后再加载真正的activity。

什么是占坑?就是构造一系列假的 Activity 替身,在 AndroidMainfest.xml 里面进行注册,以绕过检测,然后到了真正启动 Activity 的时候,再把它变回,去启动真正的目标 Activity。那么这一步是怎么做的呢?

我们可以打开核心库里面的 AndroidMainfest.xml 看看:

<application><!-- Stub Activities --><activity android:name=".A$1" android:launchMode="standard"/><activity android:name=".A$2" android:launchMode="standard"android:theme="@android:style/Theme.Translucent" /><!-- Stub Activities --><activity android:name=".B$1" android:launchMode="singleTop"/><activity android:name=".B$2" android:launchMode="singleTop"/><activity android:name=".B$3" android:launchMode="singleTop"/><activity android:name=".B$4" android:launchMode="singleTop"/><activity android:name=".B$5" android:launchMode="singleTop"/><activity android:name=".B$6" android:launchMode="singleTop"/><activity android:name=".B$7" android:launchMode="singleTop"/><activity android:name=".B$8" android:launchMode="singleTop"/><!-- Stub Activities --><activity android:name=".C$1" android:launchMode="singleTask"/><activity android:name=".C$2" android:launchMode="singleTask"/><activity android:name=".C$3" android:launchMode="singleTask"/><activity android:name=".C$4" android:launchMode="singleTask"/><activity android:name=".C$5" android:launchMode="singleTask"/><activity android:name=".C$6" android:launchMode="singleTask"/><activity android:name=".C$7" android:launchMode="singleTask"/><activity android:name=".C$8" android:launchMode="singleTask"/><!-- Stub Activities --><activity android:name=".D$1" android:launchMode="singleInstance"/><activity android:name=".D$2" android:launchMode="singleInstance"/><activity android:name=".D$3" android:launchMode="singleInstance"/><activity android:name=".D$4" android:launchMode="singleInstance"/><activity android:name=".D$5" android:launchMode="singleInstance"/><activity android:name=".D$6" android:launchMode="singleInstance"/><activity android:name=".D$7" android:launchMode="singleInstance"/><activity android:name=".D$8" android:launchMode="singleInstance"/></application>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

可以发现,在清单里面注册了一堆假的 Activity。 ABCD分别对应不同的启动模式,那么,我们启动插件的 Activity 的时候,是如何把它改为清单里面已注册的这些假的 Activity 名呢?

VAInstrumentation 里面,重写了 startActivity 的必经之路,就是 execStartActivity() 方法:

public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,Intent intent, int requestCode, Bundle options) {// 这里面做了一系列操作,实际上就是查找插件里面第一个符合隐式条件的第一个ResolveInfo,并设置进intentmPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);// null component is an implicitly intentif (intent.getComponent() != null) {Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(),intent.getComponent().getClassName()));// !!! 重头戏在这里,用那些注册的假的StubActivity来替换真实的Activity,以绕过检测 !!!this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);}ActivityResult result = realExecStartActivity(who, contextThread, token, target,intent, requestCode, options);return result;}private ActivityResult realExecStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,Intent intent, int requestCode, Bundle options) {ActivityResult result = null;try {Class[] parameterTypes = {Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class,int.class, Bundle.class};result = (ActivityResult)ReflectUtil.invoke(Instrumentation.class, mBase,"execStartActivity", parameterTypes,who, contextThread, token, target, intent, requestCode, options);} catch (Exception e) {e.printStackTrace();}return result;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

那么,是如何替换 StubActivity 的呢? 跟进代码:

public void markIntentIfNeeded(Intent intent) {if (intent.getComponent() == null) {return;}String targetPackageName = intent.getComponent().getPackageName();String targetClassName = intent.getComponent().getClassName();// 判断是否是启动插件的Activityif (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {// 做标记intent.putExtra(Constants.KEY_IS_PLUGIN, true);// 保存真实的意图intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);dispatchStubActivity(intent);}
}/*** 真正的转换就在这里。根据启动模式,转换对应的 StubActivity*/
private void dispatchStubActivity(Intent intent) {ComponentName component = intent.getComponent();String targetClassName = intent.getComponent().getClassName();LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(intent);ActivityInfo info = loadedPlugin.getActivityInfo(component);if (info == null) {throw new RuntimeException("can not find " + component);}int launchMode = info.launchMode;Resources.Theme themeObj = loadedPlugin.getResources().newTheme();themeObj.applyStyle(info.theme, true);// 实际上就是这一句,完成转换String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));intent.setClassName(mContext, stubActivity);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

继续跟进代码:

class StubActivityInfo {public static final int MAX_COUNT_STANDARD = 1;public static final int MAX_COUNT_SINGLETOP = 8;public static final int MAX_COUNT_SINGLETASK = 8;public static final int MAX_COUNT_SINGLEINSTANCE = 8;public static final String corePackage = "com.didi.virtualapk.core";// 这个格式,就是那些假的Activity的名字public static final String STUB_ACTIVITY_STANDARD = "%s.A$%d";public static final String STUB_ACTIVITY_SINGLETOP = "%s.B$%d";public static final String STUB_ACTIVITY_SINGLETASK = "%s.C$%d";public static final String STUB_ACTIVITY_SINGLEINSTANCE = "%s.D$%d";public final int usedStandardStubActivity = 1;public int usedSingleTopStubActivity = 0;public int usedSingleTaskStubActivity = 0;public int usedSingleInstanceStubActivity = 0;private HashMap<String, String> mCachedStubActivity = new HashMap<>();public String getStubActivity(String className, int launchMode, Theme theme) {String stubActivity= mCachedStubActivity.get(className);if (stubActivity != null) {return stubActivity;}TypedArray array = theme.obtainStyledAttributes(new int[]{android.R.attr.windowIsTranslucent,android.R.attr.windowBackground});boolean windowIsTranslucent = array.getBoolean(0, false);array.recycle();if (Constants.DEBUG) {Log.d("StubActivityInfo", "getStubActivity, is transparent theme ? " + windowIsTranslucent);}stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);switch (launchMode) {case ActivityInfo.LAUNCH_MULTIPLE: {stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);if (windowIsTranslucent) {stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2);}break;}case ActivityInfo.LAUNCH_SINGLE_TOP: {usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);break;}case ActivityInfo.LAUNCH_SINGLE_TASK: {usedSingleTaskStubActivity = usedSingleTaskStubActivity % MAX_COUNT_SINGLETASK + 1;stubActivity = String.format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity);break;}case ActivityInfo.LAUNCH_SINGLE_INSTANCE: {usedSingleInstanceStubActivity = usedSingleInstanceStubActivity % MAX_COUNT_SINGLEINSTANCE + 1;stubActivity = String.format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity);break;}default:break;}mCachedStubActivity.put(className, stubActivity);return stubActivity;}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

到这一步,就基本清晰了。同样的,既然变为了 StubActivity,那么真正启动的时候还得变回来才行。来看一下重写后的 newActivity() 方法:

@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {try {cl.loadClass(className);} catch (ClassNotFoundException e) {// 根据 intent 类型,去获取相应的插件LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);// 这里就是从Intent中取出我们刚才保存的真正的意图String targetClassName = PluginUtil.getTargetActivity(intent);Log.i(TAG, String.format("newActivity[%s : %s]", className, targetClassName));if (targetClassName != null) {// mBase 是未替换之前的 Instrumentation 对象,所以这个实际上是交给系统原先的 Instrumentation 对象去执行,所以这个模式其实也可以理解为与动态代理等同// plugin.getClassLoader() 是自己构造的一个 DexClassLoader,专门用于加载对应的apk里面的类Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);activity.setIntent(intent);try {// for 4.1+ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());} catch (Exception ignored) {// ignored.}return activity;}}return mBase.newActivity(cl, className, intent);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

到这里,插件的 Activity 启动流程分析,就基本结束了。细节方面,没法一步到位,还需要大家边看源码边理解,这样才能看得更透彻。

Service 支持

对于 Service 的支持,采用动态代理AMS,拦截 Service 相关的请求,将其中转给Service Runtime去处理,Service Runtime会接管系统的所有操作。

对于我们动态代理AMS,在上一节 Activity支持 中已经介绍过了,那么,简单的来看一下 ActivityManagerProxy 是如何启动一个Service的。

在执行 startService 等方法的时候,AMS 代理对象会相应的来执行以下这些方法:

private Object startService(Object proxy, Method method, Object[] args) throws Throwable {IApplicationThread appThread = (IApplicationThread) args[0];Intent target = (Intent) args[1];ResolveInfo resolveInfo = this.mPluginManager.resolveService(target, 0);if (null == resolveInfo || null == resolveInfo.serviceInfo) {// is host servicereturn method.invoke(this.mActivityManager, args);}return startDelegateServiceForTarget(target, resolveInfo.serviceInfo, null, RemoteService.EXTRA_COMMAND_START_SERVICE);
}private ComponentName startDelegateServiceForTarget(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {Intent wrapperIntent = wrapperTargetIntent(target, serviceInfo, extras, command);return mPluginManager.getHostContext().startService(wrapperIntent);
}private Intent wrapperTargetIntent(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {// fill in service with ComponentNametarget.setComponent(new ComponentName(serviceInfo.packageName, serviceInfo.name));String pluginLocation = mPluginManager.getLoadedPlugin(target.getComponent()).getLocation();// 这里进行判断,看是交给 LocalService,还是 RemoteService 处理boolean local = PluginUtil.isLocalService(serviceInfo);Class<? extends Service> delegate = local ? LocalService.class : RemoteService.class;Intent intent = new Intent();intent.setClass(mPluginManager.getHostContext(), delegate);intent.putExtra(RemoteService.EXTRA_TARGET, target);// 保存一下这个的Command,对应执行不同操作intent.putExtra(RemoteService.EXTRA_COMMAND, command);intent.putExtra(RemoteService.EXTRA_PLUGIN_LOCATION, pluginLocation);if (extras != null) {intent.putExtras(extras);}return intent;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

实际上包括我们调用 stopService,AMS 代理对象最后变换后的意图,同样也是上面代码的最后两个个方法 startDelegateServiceForTargetwrapperTargetIntent(),只不过 command 不一样。

所以本质上 AMS 作为代理,不管你执行启动或者关闭插件里面的 Service,他都是调用 LocalService 或者 RemoteService 的 startService 方法,在 LocalService 或者 RemoteService 的 onStartCommand 下,根据command 进行相应的操作。那么我们来看一下 LocalService 的 onStartCommand 方法:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {if (null == intent || !intent.hasExtra(EXTRA_TARGET) || !intent.hasExtra(EXTRA_COMMAND)) {return START_STICKY;}Intent target = intent.getParcelableExtra(EXTRA_TARGET);int command = intent.getIntExtra(EXTRA_COMMAND, 0);if (null == target || command <= 0) {return START_STICKY;}ComponentName component = target.getComponent();LoadedPlugin plugin = mPluginManager.getLoadedPlugin(component);switch (command) {case EXTRA_COMMAND_START_SERVICE: {ActivityThread mainThread = (ActivityThread)ReflectUtil.getActivityThread(getBaseContext());IApplicationThread appThread = mainThread.getApplicationThread();Service service;if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {service = this.mPluginManager.getComponentsHandler().getService(component);} else {try {service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance();Application app = plugin.getApplication();IBinder token = appThread.asBinder();Method attach = service.getClass().getMethod("attach", Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class);IActivityManager am = mPluginManager.getActivityManager();attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);service.onCreate();this.mPluginManager.getComponentsHandler().rememberService(component, service);} catch (Throwable t) {return START_STICKY;}}service.onStartCommand(target, 0, this.mPluginManager.getComponentsHandler().getServiceCounter(service).getAndIncrement());break;}case EXTRA_COMMAND_BIND_SERVICE: {ActivityThread mainThread = (ActivityThread)ReflectUtil.getActivityThread(getBaseContext());IApplicationThread appThread = mainThread.getApplicationThread();Service service = null;if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {service = this.mPluginManager.getComponentsHandler().getService(component);} else {try {service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance();Application app = plugin.getApplication();IBinder token = appThread.asBinder();Method attach = service.getClass().getMethod("attach", Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class);IActivityManager am = mPluginManager.getActivityManager();attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);service.onCreate();this.mPluginManager.getComponentsHandler().rememberService(component, service);} catch (Throwable t) {t.printStackTrace();}}try {IBinder binder = service.onBind(target);IBinder serviceConnection = PluginUtil.getBinder(intent.getExtras(), "sc");IServiceConnection iServiceConnection = IServiceConnection.Stub.asInterface(serviceConnection);iServiceConnection.connected(component, binder);} catch (Exception e) {e.printStackTrace();}break;}case EXTRA_COMMAND_STOP_SERVICE: {Service service = this.mPluginManager.getComponentsHandler().forgetService(component);if (null != service) {try {service.onDestroy();} catch (Exception e) {Log.e(TAG, "Unable to stop service " + service + ": " + e.toString());}} else {Log.i(TAG, component + " not found");}break;}case EXTRA_COMMAND_UNBIND_SERVICE: {Service service = this.mPluginManager.getComponentsHandler().forgetService(component);if (null != service) {try {service.onUnbind(target);service.onDestroy();} catch (Exception e) {Log.e(TAG, "Unable to unbind service " + service + ": " + e.toString());}} else {Log.i(TAG, component + " not found");}break;}}return START_STICKY;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107

很显然,在这里面才对应去控制了插件Service的生命周期。具体代码就留给大家分析吧~~

ContentProvider 支持

动态代理 IContentProvider,拦截provider相关的请求,将其中转给Provider Runtime去处理,Provider Runtime会接管系统的所有操作。

我们来看一下 com.didi.virtualapk.internal.PluginContentResolver 这个类:

public class PluginContentResolver extends ContentResolver {private ContentResolver mBase;private PluginManager mPluginManager;private static Method sAcquireProvider;private static Method sAcquireExistingProvider;private static Method sAcquireUnstableProvider;static {try {sAcquireProvider = ContentResolver.class.getDeclaredMethod("acquireProvider",new Class[]{Context.class, String.class});sAcquireProvider.setAccessible(true);sAcquireExistingProvider = ContentResolver.class.getDeclaredMethod("acquireExistingProvider",new Class[]{Context.class, String.class});sAcquireExistingProvider.setAccessible(true);sAcquireUnstableProvider = ContentResolver.class.getDeclaredMethod("acquireUnstableProvider",new Class[]{Context.class, String.class});sAcquireUnstableProvider.setAccessible(true);} catch (Exception e) {//ignored}}public PluginContentResolver(Context context) {super(context);mBase = context.getContentResolver();mPluginManager = PluginManager.getInstance(context);}protected IContentProvider acquireProvider(Context context, String auth) {try {if (mPluginManager.resolveContentProvider(auth, 0) != null) {// 在这里,去 hook 一个 IContentProvider 代理对象return mPluginManager.getIContentProvider();}return (IContentProvider) sAcquireProvider.invoke(mBase, context, auth);} catch (Exception e) {e.printStackTrace();}return null;}// ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

这个类是在构造 LoadedPlugin 的时候创建的 PluginContext 对象里面的 getContentResolver() 里面创建的。

class PluginContext extends ContextWrapper {private final LoadedPlugin mPlugin;public PluginContext(LoadedPlugin plugin) {super(plugin.getPluginManager().getHostContext());this.mPlugin = plugin;}@Overridepublic ContentResolver getContentResolver() {// 创建代理支持return new PluginContentResolver(getHostContext());}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

那么,上面Hook 的 IContentProvider 代理对象,实际上是在 PluginManager 做的。

private void hookIContentProviderAsNeeded() {Uri uri = Uri.parse(PluginContentResolver.getUri(mContext));mContext.getContentResolver().call(uri, "wakeup", null, null);try {Field authority = null;Field mProvider = null;ActivityThread activityThread = (ActivityThread) ReflectUtil.getActivityThread(mContext);Map mProviderMap = (Map) ReflectUtil.getField(activityThread.getClass(), activityThread, "mProviderMap");Iterator iter = mProviderMap.entrySet().iterator();while (iter.hasNext()) {Map.Entry entry = (Map.Entry) iter.next();Object key = entry.getKey();Object val = entry.getValue();String auth;if (key instanceof String) {auth = (String) key;} else {if (authority == null) {authority = key.getClass().getDeclaredField("authority");authority.setAccessible(true);}auth = (String) authority.get(key);}if (auth.equals(PluginContentResolver.getAuthority(mContext))) {if (mProvider == null) {mProvider = val.getClass().getDeclaredField("mProvider");mProvider.setAccessible(true);}IContentProvider rawProvider = (IContentProvider) mProvider.get(val);IContentProvider proxy = IContentProviderProxy.newInstance(mContext, rawProvider);mIContentProvider = proxy;Log.d(TAG, "hookIContentProvider succeed : " + mIContentProvider);break;}}} catch (Exception e) {e.printStackTrace();}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

这一块的内容,最好根据滴滴提供的Demo,再来看,比较容易理解。

Uri bookUri = Uri.parse("content://com.ryg.chapter_2.book.provider/book");
ContentValues values = new ContentValues();
values.put("_id", 6);
values.put("name", "程序设计的艺术");
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

哈哈,作者有点懒,用了任玉刚的《Android开发艺术探索》 改的,被发现了

Receiver 支持

官方解释是将插件中静态注册的receiver重新注册一遍。在代码里貌似没找到相应的支持,Demo 里也没有,或许这部分还没完成吧??

小结

本文重点在于分析插件的 Activity 启动流程,其他包括主题、资源,并没有详细分析,因为说细了内容还是有点多了,主要是让大伙儿在阅读代码时,有个大致的方向。有疑问欢迎一起探讨哟~~

感谢阅读!

1
1
相关文章推荐
  • 滴滴插件化框架VirtualAPK原理解析(一)之插件Activity管理
  • 关闭vmware虚拟机滴滴声 bell 声音
  • VirtualAPK:滴滴 Android 插件化的实践之路
  • 滴滴打车的架构变迁
  • 血战打的软件市场滴滴vs快的的烧钱之战
  • 去除 linux中滴滴声音解决办法
  • Android GitHub汇总
  • 我的足球小滴滴
  • Android 插件化学习
  • 插件系统框架分析

猜你在找
机器学习之概率与统计推断
机器学习之数学基础
机器学习之凸优化
机器学习之矩阵
响应式布局全新探索
探究Linux的总线、设备、驱动模型
深度学习基础与TensorFlow实践
深度学习之神经网络原理与实战技巧
前端开发在线峰会
TensorFlow实战进阶:手把手教你做图像识别应用
查看评论

  暂无评论

发表评论
  • 用 户 名:
  • wzx104104104
  • 评论内容:
  • 插入代码
    HTML/XMLobjective-cDelphiRubyPHPC#C++JavaScriptVisual BasicPythonJavaCSSSQL其它

* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场

快速回复 TOP

  • 个人资料

  • LeBron_Six

    • 访问:525366次
    • 积分:5120
    • 等级:

      积分:5120

    • 排名:第5153名
    • 原创:71篇
    • 转载:2篇
    • 译文:4篇
    • 评论:342条
  • GitHub
  • LeBron_Six
    Android Dev.
    HangZhou , China

    312
    Followers

    6
    Following

    20
    Repos


    Popular Repositories
    BookReader
    Java
    ★2927

    ImageSelector
    Java
    ★591

    SprintNBA
    Java
    ★432

    BankCardFormat
    Java
    ★174

    Last active: 52 day(s) ago

  • Google最新hosts镜像(自动在线更新)
  • 我的微博
  • 文章分类
  • Android自定义控件(6)
  • Android 杂谈(47)
  • PhoneGap(9)
  • 编程语言(6)
  • Mac OS X(3)
  • 支付行业(3)
  • Android 项目(6)
  • react-native(0)
  • 阅读排行
  • 解决AndroidStudio导入项目在 Building gradle project info 一直卡住(51691)
  • Error:Execution failed for task ':app:processDebugResources'. 的解决办法(27114)
  • Android Studio在创建/导入项目的时候,一直处于building “XXX”gradle project info的解决办法(26781)
  • Android 通过JNI实现守护进程(25359)
  • Android实现应用的增量更新\升级(19042)
  • RxJava 从入门到出轨(17329)
  • Android 无需root实现apk的静默安装(14835)
  • 解决电脑无法自动获取IP地址(13855)
  • Android 源代码分享(13760)
  • Android Studio 使用正式签名进行调试(13238)
  • 最新评论
  • 解决 adb not responding. if you'd like to retry then please manually kill adb.exe and click 'restart'

    呆呆瓜_小司: 多谢楼主,解决了我的问题

  • Android 通过JNI实现守护进程

    lijinyun2009: 楼主能否发一下源码,学习一下 798783825@qq.com

  • 解决AndroidStudio导入项目在 Building gradle project info 一直卡住

    xz3812093h: 第一种方法实测没问题

  • Android Studio在创建/导入项目的时候,一直处于building “XXX”gradle project info的解决办法

    csdnqixiaoxin: 赞,问题解决了^_^

  • Android 图片选择器,丰富的配置选项,极大程度的简化使用

    初见梦想: 楼主你好,想知道相册选出来的图片有没有进行压缩?

  • 解决AndroidStudio导入项目在 Building gradle project info 一直卡住

    抠掉三技能的山兔兔: 用了方法二,修改properties,可行,谢谢楼主

  • Android 无需root实现apk的静默安装

    _弘霖: 楼主,按照您的demo,,IPackageManager报错是怎么回事

  • Android 无需root实现apk的静默安装

    _弘霖: 楼主,按照你的demo,没有IPackageManager呀,api24

  • RxJava 从入门到出轨

    LeBron_Six: @qq_15040737:这个好像用的地方不是很多吧。RxJava主要在于异步、变换与线程调度,BS...

  • RxJava 从入门到出轨

    我是有涵养的程序猿: 非常感谢楼主的总结的分享,想问下如果是 browser/server 模式的应用 有什么应用场景?

  • 欢迎访问我的博客

您有1条新通知

收藏助手

保存代码片

整理和分享保存的代码片,请访问代码笔记

  • *标题
  • *描述
  • 标签
    VirtualAPKxAndroidx插件化x滴滴x代理x
取消确定

提问

您的问题将会被发布在“技术问答”频道×

该问题已存在,请勿重复提问

插入图片

||||||
 

000:0

推荐标签:

我要悬赏

取消发布

可能存在类似的问题:
我想提一个新问题

滴滴开源Android插件框架相关推荐

  1. 滴滴开源Android插件化框架VirtualAPK原理分析

    概述 滴滴出行公司的首个对外开源项目 - VirtualAPK.地址:github.com/didi/Virtua- 滴滴自行研发了这款插件化框架,功能全面.兼容性好,还能够适用于有耦合的业务插件,这 ...

  2. 腾讯零反射全动态Android插件框架Shadow解析

    简介 最近几年,腾讯对于开源事业也是越来越支持,今天要说的就是在腾讯被广泛使用的Shadow框架,一个经过线上亿级用户量检验的反射全动态Android插件框架. 首先,让我们来看一下官方对于Shado ...

  3. Android 插件框架机制之Small

    Android 插件框架机制系列文章: Android 插件框架机制之预热篇 Android 插件框架机制之DroidPlugin 引言 上一篇文章提到过Small,这次就简单说一下Small,这只是 ...

  4. Android插件框架VirtualAPK

    VirtualAPK是滴滴出行自研的一款优秀的插件化框架,主要有如下几个特性. 功能完备 支持几乎所有的Android特性: 四大组件方面 四大组件均不需要在宿主manifest中预注册,每个组件都有 ...

  5. android插件框架机制的选择,Android插件开发初探——基础篇

    Android插件开发初探 对于Android的插件化其实已经讨论已久了,但是市面上还没有非常靠谱成熟的插件框架供我们使用.这里我们就尝试性的对比一下Java中,我们使用插件化该是一个怎么样的流程,且 ...

  6. 基于Proxy思想的Android插件框架

    本文所有代码托管在Github:android-plugin 意义 研究插件框架的意义在于以下几点: 减小安装包的体积,通过网络选择性地进行插件下发 模块化升级,减小网络流量 静默升级,用户无感知情况 ...

  7. Android 插件框架实现思路及原理

    插件框架实现思路及原理 一.技术可行性 a) apk的安装处理流程 i. apk会copy到/data/app: ii. 解压apk中的class.dex,并对其进行优化,获得odex(即JIT).最 ...

  8. 滴滴开源小程序框架 Mpx

    Mpx 是一款致力于提高小程序开发体验的增强型小程序框架,通过 Mpx,能够以最先进的 Web 开发体验 ( Vue + Webpack ) 来开发生产性能深度优化的小程序,Mpx 具有以下一些优秀特 ...

  9. NoHttp开源Android网络框架1.0.0之架构分析

    转载于:https://www.cnblogs.com/brucemengbm/p/7091521.html

最新文章

  1. 设置系统和管理计算机硬件的应用程序,Windows7操作系统中用于设置系统和管理计算机硬件的应用程序是()...
  2. java scanner和for_java中Scanner和random的用法
  3. 字符串与数组的常用方法
  4. 【TypeScript】防止对象改变
  5. shell脚本每日一练(三)
  6. selenium+java初级学习笔记之单个元素定位
  7. 解决加载静态文件无法被浏览器缓存问题
  8. Dollar toolbox 学习笔记(一)
  9. (24)Verilog HDL条件语句:case语句
  10. VSCode Debug
  11. 《Android软件安全与逆向分析》— Android 书籍
  12. AJAX 必用的情况(待选........)
  13. 翟菜花:搭上营销快通车的乳业,又是如何玩转互联网营销时代的?
  14. 帝国cms7.2密码修改
  15. c语言中dot作用,Unix中的dot命令详解
  16. 已解决TypeError: Descriptors cannot not be created directly.
  17. java 源文件结构_A005Java源文件结构
  18. linux怎么查看是不是centos版本
  19. 男人“杀死”女人的30句话(zt)
  20. 数字信号处理(五)快速傅里叶变换

热门文章

  1. 区块链ICO:互联网进化的驱动力
  2. 记一次某公众号平台前端加密算法的解密
  3. 小程序步数解密php,微信小程序--获取微信运动步数的实例代码
  4. TCP: too many of orphaned sockets错误
  5. 医院pacs系统服务器配置,浪潮为千佛山医院PACS系统开“药方”
  6. 2021-10-11 今日总结
  7. 编写应用程序,计算两个非零正整数的最大公约数和最小公倍数,要求两个非零正整数从键盘输入。
  8. C++基础课 5- 章
  9. 制作多关卡系统 func_brush
  10. 阿里云服务器购买了还需要买数据库吗?