文章目录

  • DroidPlugin原理解析
    • 插件包安装
    • 插件包解析和加载
    • 插件Activity启动解析
    • 插件service启动分析
    • 插件receiver分析
    • 插件provider分析
    • 插件加载独立性
    • 插件resource获取
    • 总结

DroidPlugin原理解析

从系统设计的角度,组件和窗口的逻辑实体都存在于系统服务,比如Activity创建后,其逻辑控制主体在AMS,对于窗口,其逻辑控制主体在WMS

android将逻辑主体放置于系统服务,系统就可以对组件和窗口的生命周期,显示状态进行强掌控,这样就能做到在各种状态变更时能做到及时回调通知

所以,创建任何组件,都需要通过RPC通讯到AMS创建 — 第一个hook点

那逻辑主体确定后,AMS就需要创建进程去运行真实的Activity对象(可以认为它是一个提线木偶)

Android进程启动后,JAVA的入口是ActivityThread.main

ActivityThread主要干两件事件

  • 创建IApplicationThread native binder和AMS进行通讯
  • 收到AMS发来的RPC事件后,创建并保存各个组件相关的数据 ---- 第二个hook点

组件相关数据主要包括两个

  • 组件所属包信息和对应的loadedApk - 保存于mPackages
  • 将AMS中逐渐的逻辑主体对象token和真实组件对象一同保存,便于后续跟踪操作 - 比如Activity相关的保存于mActivities,service相关保存于mServices

还有,ActivityThread的设计本身好像就支持加载多个application,多个application会被保存到mAllApplications中

插件包安装

DroidPlugin实现了一个简易的IPluginManagerImpl用于插件APK包的安装和解析,当然这部分代码是参考系统的PMS来实现的,主要职责:

  • 插件APK安装到本地目录
  • 对插件APK的组件等数据进行解析

插件包解析和加载

  1. 插件包的解析,就是对AndroidManifest的解析,主要通过反射系统的PackageParser来完成
  2. 在Activity启动前(hook见下面介绍),会调用
 PluginProcessManager.preLoadApk(mHostContext, targetActivityInfo);preLoadApk内部会根据targetActivityInfo包含的包名来判断LoadedApk是否创建,如果未创建,则会通过反射调用ActivityThread的函数来创建插件LoadedApk并保存到ActivityThread的mPackages中,接着创建PluginClassLoader并设置到LoadedApk对象中
  1. 最后通过反射调用LoadedApk的makeApplication创建插件Application对象并调用onCreate

插件Activity启动解析

我们先来看下Android常规Activity的启动流程

  1. 调用Context.startActivity -> ActivityManagerNative -> AMS, AMS通过Intent从PMS拿到ActivityInfo并创建ActivityRecord和token放入前台ActivityStack,接着按需启动Activity所属进程
  2. 进程启动后,马上执行入口ActivityThread.main并调用attachApplication将启动信息反馈到AMS,AMS通过pid找到对应的ProcessRecord并更新其数据
  3. 接着从前台ActivityStack中拿到栈顶的ActivityRecord,如果其proecssrecord为null,并且uid和processname跟新创建的ProcessRecord一致,则正式调用app.thread.scheduleLaunchActivity
  4. ActivityThread在scheduleLaunchActivity中创建ActivityClientRecord,用于跟AMS中的ActivityRecord对应,ActivityClientRecord最重要的两个字段是token和activityinfo,token用于关联ActivityRecord,activityinfo则包含activity的描述和所属包等信息
  5. 在scheduleLaunchActivity内部接着发送LAUNCH_ACTIVITY message到mH这个handler,mH收到LAUNCH_ACTIVITY message后的代码如下:
     ActivityClientRecord r = (ActivityClientRecord)msg.obj;//通过activityinfo中包含的application信息创建loaedapk并保存于packageinfor.packageInfo = getPackageInfoNoCheck(r.activityInfo.applicationInfo, r.compatInfo);handleLaunchActivity(r, null);
    

理解上面第1和第5步很重要,因为DroidPlugin的Activity hook就是基于这两个点来进行的,原理总结如下:

  1. DroidPlugin首先在host app的AndroidManifest预注册一堆stub
    activity,这里只列出一部分,详细的可查看源码

      .stub.ActivityStub$P00$Standard00.stub.ActivityStub$P00$SingleInstance00.stub.ActivityStub$P00$SingleInstance01.stub.ActivityStub$P00$SingleInstance02.stub.ActivityStub$P00$SingleInstance03.stub.ActivityStub$P00$SingleTask00.stub.ActivityStub$P00$SingleTask01.stub.ActivityStub$P00$SingleTask02.stub.ActivityStub$P00$SingleTask03.stub.ActivityStub$P00$SingleTop00.stub.ActivityStub$P00$SingleTop01.stub.ActivityStub$P00$SingleTop02.stub.ActivityStub$P00$SingleTop03
    
  2. 通过动态代理和反射,hook ActivityManagerNative的接口,这个实现原理网上很多,这里不再赘述
  3. hook startActivity,相关代码在IActivityManagerHookHandle.startActivity中
                 ActivityInfo activityInfo = resolveActivity(intent);if (activityInfo != null && isPackagePlugin(activityInfo.packageName)) {ComponentName component = selectProxyActivity(intent);if (component != null) {Intent newIntent = new Intent();try {ClassLoader pluginClassLoader = PluginProcessManager.getPluginClassLoader(component.getPackageName());setIntentClassLoader(newIntent, pluginClassLoader);} catch (Exception e) {Log.w(TAG, "Set Class Loader to new Intent fail", e);}newIntent.setComponent(component);newIntent.putExtra(Env.EXTRA_TARGET_INTENT, intent);newIntent.setFlags(intent.getFlags());String callingPackage = (String) args[1];if (TextUtils.equals(mHostContext.getPackageName(), callingPackage)) {newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);args[intentOfArgIndex] = newIntent;args[1] = mHostContext.getPackageName();}
    
    • 根据intent从DroidPlugin的packagemanager中拿到activityinfo(如果已安装插件包中有匹配的activty)
    • 还是根据intent,根据目标activity的属性,去匹配一个最合适的stub activity,并将component信息保存到newIntent,同时将intent作为extra保存到newintent
    • 最后将args中intent替换称newintent达到偷梁换柱的效果

      经过上面的偷梁换柱后,系统实际上拿到的是newintent,进而启动stubactivity;DroidPlugin接下去要做的就是,将stubactivity还原成真正要启动的插件activity,这个是在上面启动流程第5步中完成的

  4. 上面启动流程第五部可以看出,ActivityThread在启动Activity的时候,最重要的两个参数就是ActivityClientRecord里的两个变量intent和activityinfo,activityinfo是用来创建packageinfo(loadedapk), intent是要在创建activity后传入的,所以DroidPlugin必须要在创建Acivity之前,也就是handleLaunchActivity(msg)之前将这两个变量替换成原始的插件intent,这就是DroidPlugin Hook mH的目的,下面是hook 也就是handleLaunchActivity的部分代码
    //PluginCallback.java
    private boolean handleLaunchActivity(Message msg) {try {Object obj = msg.obj;Intent stubIntent = (Intent) FieldUtils.readField(obj, "intent");//ActivityInfo activityInfo = (ActivityInfo) FieldUtils.readField(obj, "activityInfo", true);stubIntent.setExtrasClassLoader(mHostContext.getClassLoader());Intent targetIntent = stubIntent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);// 这里多加一个isNotShortcutProxyActivity的判断,因为ShortcutProxyActivity的很特殊,启动它的时候,// 也会带上一个EXTRA_TARGET_INTENT的数据,就会导致这里误以为是启动插件Activity,所以这里要先做一个判断。// 之前ShortcutProxyActivity错误复用了key,但是为了兼容,所以这里就先这么判断吧。if (targetIntent != null && !isShortcutProxyActivity(stubIntent)) {IPackageManagerHook.fixContextPackageManager(mHostContext);ComponentName targetComponentName = targetIntent.resolveActivity(mHostContext.getPackageManager());ActivityInfo targetActivityInfo = PluginManager.getInstance().getActivityInfo(targetComponentName, 0);if (targetActivityInfo != null) {if (targetComponentName != null && targetComponentName.getClassName().startsWith(".")) {targetIntent.setClassName(targetComponentName.getPackageName(), targetComponentName.getPackageName() + targetComponentName.getClassName());}ResolveInfo resolveInfo = mHostContext.getPackageManager().resolveActivity(stubIntent, 0);ActivityInfo stubActivityInfo = resolveInfo != null ? resolveInfo.activityInfo : null;if (stubActivityInfo != null) {PluginManager.getInstance().reportMyProcessName(stubActivityInfo.processName, targetActivityInfo.processName, targetActivityInfo.packageName);}PluginProcessManager.preLoadApk(mHostContext, targetActivityInfo);ClassLoader pluginClassLoader = PluginProcessManager.getPluginClassLoader(targetComponentName.getPackageName());setIntentClassLoader(targetIntent, pluginClassLoader);setIntentClassLoader(stubIntent, pluginClassLoader);boolean success = false;try {targetIntent.putExtra(Env.EXTRA_TARGET_INFO, targetActivityInfo);if (stubActivityInfo != null) {targetIntent.putExtra(Env.EXTRA_STUB_INFO, stubActivityInfo);}success = true;} catch (Exception e) {Log.e(TAG, "putExtra 1 fail", e);}if (!success && Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {try {ClassLoader oldParent = fixedClassLoader(pluginClassLoader);targetIntent.putExtras(targetIntent.getExtras());targetIntent.putExtra(Env.EXTRA_TARGET_INFO, targetActivityInfo);if (stubActivityInfo != null) {targetIntent.putExtra(Env.EXTRA_STUB_INFO, stubActivityInfo);}fixedClassLoader(oldParent);success = true;} catch (Exception e) {Log.e(TAG, "putExtra 2 fail", e);}}if (!success) {Intent newTargetIntent = new Intent();newTargetIntent.setComponent(targetIntent.getComponent());newTargetIntent.putExtra(Env.EXTRA_TARGET_INFO, targetActivityInfo);if (stubActivityInfo != null) {newTargetIntent.putExtra(Env.EXTRA_STUB_INFO, stubActivityInfo);}FieldUtils.writeDeclaredField(msg.obj, "intent", newTargetIntent);} else {FieldUtils.writeDeclaredField(msg.obj, "intent", targetIntent);}FieldUtils.writeDeclaredField(msg.obj, "activityInfo", targetActivityInfo);Log.i(TAG, "handleLaunchActivity OK");} else {Log.e(TAG, "handleLaunchActivity oldInfo==null");}} else {Log.e(TAG, "handleLaunchActivity targetIntent==null");}} catch (Exception e) {Log.e(TAG, "handleLaunchActivity FAIL", e);}if (mCallback != null) {return mCallback.handleMessage(msg);} else {return false;}}
    

    先用intent中拿出之前保存到extra的插件intent

    Intent targetIntent = stubIntent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);
    

    接着根据targetIntent获取对应的activityinfo

    ComponentName targetComponentName = targetIntent.resolveActivity(mHostContext.getPackageManager());
    ActivityInfo targetActivityInfo = PluginManager.getInstance().getActivityInfo(targetComponentName, 0);
    

    最后将数据写回到ActivityClientRecord,完成最终的替换

     FieldUtils.writeDeclaredField(msg.obj, "intent", targetIntent);FieldUtils.writeDeclaredField(msg.obj, "activityInfo", targetActivityInfo);
    

插件service启动分析

同样的,先来看看service的常规启动流程

  • 调用contextimpl.startService/bindService/stopService -> AMS,AMS对应创建ServiceRecord和token后,通知ActivityThread
  • ActivityThread收到startService后,会创建service并保存到mService map,key为token,接着调用oncreate
  • ActivityThread接着收到handleServiceArgs, 根据token拿到service,接着调用onStartCommond并传入intent
  • ActivityThread收到bindservice后,从根据token拿到service,接着调用onbind拿到native binder,接着调用publishService将native binder传到AMS
 ActivityManagerNative.getDefault().publishService(data.token, data.intent, binder);

Service跟Activity还是存在很大的区别的,service非常独立,也就是说,系统创建service后,除了调用规定的那些回调,传递intent外,剩下就是service自己玩自己的,跟系统一毛钱关系都没有了

Activity则不同,因为其涉及到窗口,所以会存在大量的交互,比如WMS,IMS等

对于DroidPlugin来说,插件service的hook,则会简单很多,只需要用一个stub service做为代理,在stubservice内部根据传入的intent去管理插件service对象即可:

.stub.ServiceStub$StubP00$P00

在startservice和bindservice时,只需要把目标sevice缓存stubservice,并将真实的intent作为extra传递到stub service就可以了

  private static ServiceInfo replaceFirstServiceIntentOfArgs(Object[] args) throws RemoteException {int intentOfArgIndex = findFirstIntentIndexInArgs(args);if (args != null && args.length > 1 && intentOfArgIndex >= 0) {Intent intent = (Intent) args[intentOfArgIndex];ServiceInfo serviceInfo = resolveService(intent);if (serviceInfo != null && isPackagePlugin(serviceInfo.packageName)) {ServiceInfo proxyService = selectProxyService(intent);if (proxyService != null) {Intent newIntent = new Intent();//FIXBUG:https://github.com/Qihoo360/DroidPlugin/issues/122//如果插件中有两个Service:ServiceA和ServiceB,在bind ServiceA的时候会调用ServiceA的onBind并返回其IBinder对象,// 但是再次bind ServiceA的时候还是会返回ServiceA的IBinder对象,这是因为插件系统对多个Service使用了同一个StubService// 来代理,而系统对StubService的IBinder做了缓存的问题。这里设置一个Action则会穿透这种缓存。newIntent.setAction(proxyService.name + new Random().nextInt());newIntent.setClassName(proxyService.packageName, proxyService.name);newIntent.putExtra(Env.EXTRA_TARGET_INTENT, intent);newIntent.setFlags(intent.getFlags());args[intentOfArgIndex] = newIntent;return serviceInfo;}}}return null;}

接着在stubservice会创建ServcesManager用于插件service管理,所有的stub service回调会同步到ServcesManager里:

   public int onStart(Context context, Intent intent, int flags, int startId) throws Exception {Intent targetIntent = intent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);if (targetIntent != null) {ServiceInfo targetInfo = PluginManager.getInstance().resolveServiceInfo(targetIntent, 0);if (targetInfo != null) {Service service = mNameService.get(targetInfo.name);if (service == null) {handleCreateServiceOne(context, intent, targetInfo);}handleOnStartOne(targetIntent, flags, startId);}}return -1;}

看到没,ServcesManager自己管理mNameService map,service信息则是通过extr中中真实的插件intent来获得,onbind函数同样:

 public IBinder onBind(Context context, Intent intent) throws Exception {Intent targetIntent = intent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);if (targetIntent != null) {ServiceInfo info = PluginManager.getInstance().resolveServiceInfo(targetIntent, 0);Service service = mNameService.get(info.name);if (service == null) {handleCreateServiceOne(context, intent, info);}return handleOnBindOne(targetIntent);}return null;}

这两个函数在mNameService未包含该service实例的时候,都会调用handleCreateServiceOne,通过反射调用ActivityThrea的方法创建service,从而达到调用oncreate的目地

插件receiver分析

在插件apk被启动的时候,会通过分析查看apk的receiver组件信息,然后动态注册

插件provider分析

先介绍ContentProvider的实现原理

  • 本质肯定是基于binder,所以每一个ContentProvider都会实现Transport native binder
  • 当我们调用getContentResolve.insert/delete等操作时,前提肯定是需要根据authority来拿到对应ContentProvider绑定的Transport对应binder proxy
  • 拿到binder proxy后,数据连接建立

数据连接建立后,后续跟系统也没一毛钱关系了,那理论上provider跟service是一样的,只要能hook数据发送端,接收端用一个stubprovider做代理就可以搞定了

DroidPlugin定义的stubprovider

.stub.ContentProviderStub$StubP00

发送端hook,就是替换binder proxy的过程,看DroidPlugin的getContentProvider的hook代码:

  @Overrideprotected boolean beforeInvoke(Object receiver, Method method, Object[] args) throws Throwable {if (args != null) {final int index = 1;if (args.length > index && args[index] instanceof String) {String name = (String) args[index];mStubProvider = null;mTargetProvider = null;ProviderInfo info = mHostContext.getPackageManager().resolveContentProvider(name, 0);mTargetProvider = PluginManager.getInstance().resolveContentProvider(name, 0);//这里有个很坑爹的事情,就是当插件的contentprovider和host的名称一样,冲突的时候处理方式。//在Android系统上,是不会出现这种事情的,因为系统在安装的时候做了处理。而我们目前没做处理。so,在出现冲突时候的时候优先用host的。if (mTargetProvider != null && info != null && TextUtils.equals(mTargetProvider.packageName, info.packageName)) {mStubProvider = PluginManager.getInstance().selectStubProviderInfo(name);
//                        PluginManager.getInstance().reportMyProcessName(mStubProvider.processName, mTargetProvider.processName);
//                        PluginProcessManager.preLoadApk(mHostContext, mTargetProvider);if (mStubProvider != null) {args[index] = mStubProvider.authority;} else {Log.w(TAG, "getContentProvider,fake fail 1");}} else {mTargetProvider = null;Log.w(TAG, "getContentProvider,fake fail 2=%s", name);}}}return super.beforeInvoke(receiver, method, args);}

这里不管是什么请求,authority都会被改成stub provider的authority,在请求结束后,在将authority关联contentprovider对应的binder proxy设置成DroidPlugin自己的

Object provider = FieldUtils.readField(invokeResult, "provider");if (provider != null) {boolean localProvider = FieldUtils.readField(toObj, "provider") == null;IContentProviderHook invocationHandler = new IContentProviderHook(mHostContext, provider, mStubProvider, mTargetProvider, localProvider);invocationHandler.setEnable(true);Class<?> clazz = provider.getClass();List<Class<?>> interfaces = Utils.getAllInterfaces(clazz);Class[] ifs = interfaces != null && interfaces.size() > 0 ? interfaces.toArray(new Class[interfaces.size()]) : new Class[0];Object proxyprovider = MyProxy.newProxyInstance(clazz.getClassLoader(), ifs, invocationHandler);FieldUtils.writeField(invokeResult, "provider", proxyprovider);FieldUtils.writeField(toObj, "provider", proxyprovider);
}

接着在IContentProviderHook对发送uri做替换

  if (!mLocalProvider && mStubProvider != null) {final int index = indexFirstUri(args);if (index >= 0) {Uri uri = (Uri) args[index];String authority = uri.getAuthority();if (!TextUtils.equals(authority, mStubProvider.authority)) {Uri.Builder b = new Builder();b.scheme(uri.getScheme());b.authority(mStubProvider.authority);b.path(uri.getPath());b.query(uri.getQuery());b.appendQueryParameter(Env.EXTRA_TARGET_AUTHORITY, authority);b.fragment(uri.getFragment());args[index] = b.build();}}}

将uri的authority替换成stub provider的,将插件provider的authority保存到Env.EXTRA_TARGET_AUTHORITY这个parameter中

stubprovider实现就很简单了,根据Env.EXTRA_TARGET_AUTHORITY的值来创建插件provider,接着做代理就好了,这里不就贴代码了

下面是contentprovider常规初始化流程,大家可以了解下

  • ContextImpl.getContentResolver.insert->ApplicationContentResolver.acquireProvider->ActivityThread.acquireProvider->ActivityManagerNative.getContentProvider->AMS.getContentProvider
  • 接着ActivityThread.scheduleInstallProvider->ActivityThread.installProvider
  • 接着创建ContextProvider实例并获取内部native binder
  try {final java.lang.ClassLoader cl = c.getClassLoader();localProvider = (ContentProvider)cl.loadClass(info.name).newInstance();provider = localProvider.getIContentProvider();if (provider == null) {Slog.e(TAG, "Failed to instantiate class " +info.name + " from sourceDir " +info.applicationInfo.sourceDir);return null;}if (DEBUG_PROVIDER) Slog.v(TAG, "Instantiating local provider " + info.name);// XXX Need to create the correct context for this provider.localProvider.attachInfo(c, info);} catch (java.lang.Exception e) {if (!mInstrumentation.onException(null, e)) {throw new RuntimeException("Unable to get provider " + info.name+ ": " + e.toString(), e);}return null;}

从代码里可以看出getIContentProvider返回的native binder才是contentprovider数据传输的核心

  • 接着调用ActivityManagerNative.publishContentProviders将新创建的provider同步到AMS

还有一点很重要,通过AMS.getContentProvider->ActivityThread.acquireProvider,由于ActivityThread处理都是发送消息到mH,所以它是异步的,AMS.getContentProvider如果立即返回,肯定是空的,所以它必须要等待后续ActivityManagerNative.publishContentProviders执行完成后才返回,看AMS.getContentProviderImpl部分代码:

 //ActivityManagerService.getContentProviderImpl//.....前面代码没贴// Wait for the provider to be published...synchronized (cpr) {while (cpr.provider == null) {if (cpr.launchingApp == null) {return null;}try {if (conn != null) {conn.waiting = true;}cpr.wait();} catch (InterruptedException ex) {} finally {if (conn != null) {conn.waiting = false;}}}}return cpr != null ? cpr.newHolder(conn) : null;

插件加载独立性

如果插件都在主进程启动运行,可能有人会有疑问,LoadedApk会不会乱掉?答案肯定是不会的,因为这个是DroidPlugin这个实现方案的前提,咱们看LoadedApk的生成代码

 private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,ClassLoader baseLoader, boolean securityViolation, boolean includeCode) {synchronized (mResourcesManager) {WeakReference<LoadedApk> ref;if (includeCode) {ref = mPackages.get(aInfo.packageName);} else {ref = mResourcePackages.get(aInfo.packageName);}LoadedApk packageInfo = ref != null ? ref.get() : null;if (packageInfo == null || (packageInfo.mResources != null&& !packageInfo.mResources.getAssets().isUpToDate())) {if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package ": "Loading resource-only package ") + aInfo.packageName+ " (in " + (mBoundApplication != null? mBoundApplication.processName : null)+ ")");packageInfo =new LoadedApk(this, aInfo, compatInfo, this, baseLoader,securityViolation, includeCode &&(aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0);if (includeCode) {mPackages.put(aInfo.packageName,new WeakReference<LoadedApk>(packageInfo));} else {mResourcePackages.put(aInfo.packageName,new WeakReference<LoadedApk>(packageInfo));}}return packageInfo;}}

ActivityThread会保存LoadeApk的map,key就是package name,所以各个插件的LoadedApk可以独立的存在ActivityThread中

插件resource获取

Android资源获取依赖

  • resource id,即开发中用到R..
  • 还有就是context.getResource()

由于四大组件和Application这五个入口类的创建使用的是插件的class loader,那他们使用过程中用到的R.java肯定是对应插件的,这个不会有任何问题

不过context本质是ContextImpl对象实例,这个对象不是基于插件的class loader创建的,这个要注意,但是它对插件resource独立获取没任何影响,因为

  1. context实例跟组件和Application都是一对一创建的,这就导致它不可能跟其他插件混淆
  2. context.getresource本质还是使用插件package res info创建AssertManager,它跟插件也是一对一绑定的

所以,只要完成了插件LoadedApk的创建,组件运行过程中的resource就可以正常获取

总结

DroidPlugin的设计真的很巧妙,作者能构思出这种方案,对组件的初始化肯定是非常熟悉的,这套插件化方案出来也很多年了,最近看一遍,主要还是想学习作者的实现思路,同时也加深自己对组件初始化相关代码的理解

组件实现能被偷天换日是基于Android这么一个设计前提,AMS只是保存组件的逻辑对象主体,ActivityThread只是基于逻辑主体token来创建本地组件对象并做后续跟踪,这就为修改本地组件对象提供了可能

不过这种方式对系统潜入太大了,兼容性会比较差

Android插件化之DroidPlugin原理解析相关推荐

  1. [Android 插件化(二)] DroidPlugin 用法

    1 简介 关于Android插件化可以查看我的前一篇博客:  [Android 插件化(一)] DynamicLoadApk的用法 本篇介绍第二种实现插件化的框架,360公司出品的DroidPlugi ...

  2. Android 插件化框架DroidPlugin

    上一次项目迭代中,接触到了插件化框架. 使用场景:我们的app需要集成某一直播app.即在不安装第三方直播app到手机的情况下,点击我们app内部的某一连接能跳转到直播app中,运行里面原有的所有功能 ...

  3. 【Android 插件化】DroidPlugin 编译运行 ( DroidPlugin 简介 | 编译 DroidPlugin 官方示例 | 运行 DroidPlugin 官方示例 )

    文章目录 一.DroidPlugin 简介 二.DroidPlugin 编译运行 1.编译 DroidPlugin 官方示例 2.运行 DroidPlugin 官方示例 一.DroidPlugin 简 ...

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

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

  5. 解密Android插件化的实现原理

    插件化技术在国内已经是相对比较完善,而且也有比较广泛的应用了,各种插件化的开源框架也是层出不穷.虽然我个人是不太喜欢这种钻操作系统空子,或者可以说是和原生系统对着干的黑科技,但是在国内特殊的环境下,插 ...

  6. 是时候来一波Android插件化了

    是时候来一波Android插件化了 是时候来一波Android插件化了 前言 Android开发演进 模块化介绍 插件化介绍 前提技术介绍 APK构成 Manifest Application 四大组 ...

  7. Android插件化开发之动态加载三个关键问题详解

    本文摘选自任玉刚著<Android开发艺术探索>,介绍了Android插件化技术的原理和三个关键问题,并给出了作者自己发起的开源插件化框架. 动态加载技术(也叫插件化技术)在技术驱动型的公 ...

  8. Android 插件化原理解析——Activity生命周期管理

    之前的 Android插件化原理解析 系列文章揭开了Hook机制的神秘面纱,现在我们手握倚天屠龙,那么如何通过这种技术完成插件化方案呢?具体来说,插件中的Activity,Service等组件如何在A ...

  9. Android 插件化原理解析——Hook机制之AMSPMS

    在前面的文章中我们介绍了DroidPlugin的Hook机制,也就是代理方式和Binder Hook:插件框架通过AOP实现了插件使用和开发的透明性.在讲述DroidPlugin如何实现四大组件的插件 ...

最新文章

  1. 循环神经网络(RNN)简介
  2. ctex 图片裁剪 盖住文字_新媒体运营们不可或缺的图片编辑神器!
  3. Session丢失的解决办法小结
  4. 使用kubectl port-forward暴露minikube k8s service端口
  5. Qt Creator使用版本控制系统
  6. Android Studio打包和引用aar
  7. vba mysql连接字符串_分享一个VBA连接mysql数据库的方法
  8. 编程语言在中国哪种职位最多_您使用最多的是哪种“古老”编程语言?
  9. 【操作系统/OS笔记16】进程间通信(IPC),直接/间接通信与阻塞/非阻塞通信,信号,管道,消息队列,共享内存
  10. 免费且高质量的知识有的是,你只是不知道怎么找到它们!
  11. 深度 linux ansys,deepin安装ansys
  12. c语言字体透明背景,背景透明文字不透明的最佳方法兼容IE(以背景黑色透明度0.5为例)...
  13. cad计算机绘图入门,【菜鸟宝典】AutoCAD制图入门教学!
  14. 新能源汽车动力电池(热管理)热流体仿真分析-基于SCDM和STAR-CCM+热仿真分析课程(评论发链接)
  15. 标准贴片电阻电容的焊接方法
  16. 蛋白质降解技术中常用的蛋白酶(一)
  17. 自定义View之滚动刻度尺,2018/1/14 05
  18. 华为大数据HCIA题目1
  19. UE4中的GameMode、GameState、GameInstance
  20. python爬取豆瓣影评理论依据_我用Python爬取了豆瓣的影评

热门文章

  1. unetbootin制作Linux启动U盘
  2. 长期带眼镜如何保护你观赏世界的眼睛
  3. Ubuntu安装Blender并创建桌面快捷方式的详细步骤(基于官方下载.tar.xz文件)
  4. 最强最全面的Hive SQL开发指南,超四万字全面解析!
  5. 2021年安全员-B证(陕西省)考试报名及安全员-B证(陕西省)模拟考试题
  6. iOS开发-Swift进阶之枚举enum!
  7. 《软件开发工具》(第七—十四章)
  8. 广电视讯宽带实现无线路由设置
  9. Poi3.17生成word设置页边距
  10. 解决ubuntu插入耳机,两只耳机有一只没有声音或者声音偏小的问题