一键接入Tinker

   2017-01-06 0
核心提示:Tinker开源挺长时间了,使用的开发者也越来越多,对于一些小白开发者来说对接Tinker的成本还是挺高的,其中主要因素还是不能理解为什么Application要修改成ApplicationLike,以及改造后对项目中使用Application的地方也要同步修改。在上篇文章Android热补丁之

Tinker开源挺长时间了,使用的开发者也越来越多,对于一些小白开发者来说对接Tinker的成本还是挺高的,其中主要因素还是不能理解为什么Application要修改成ApplicationLike,以及改造后对项目中使用Application的地方也要同步修改。

在上篇文章 Android热补丁之Tinker原理解析 中我们已经讲解了这样做的目的以及Tinker的加载补丁的流程,本篇文章主要讲一下一键接入Tinker的实现思路。

InstantRun

我们的目的是要实现不修改Application达到替换Application的效果,在这篇文章 从Instant run谈Android替换Application和动态加载机制 中,详细讲述了如何动态替换Application,总结起来就两步:

  1. 打包时替换Application标签,插入BootstrapApplication
  2. 运行时hook系统api,将BootstrapApplication换回MyApplication

那么,我们依然可以用这套方案来实现Tinker的一键接入,动态替换Application。

实现

有了思路我们就可以敲代码了。

打包

打包时我们要改变Manifest中Application的标签值,可以通过自定义Gradle插件来实现,关键代码

@TaskAction
    def updateManifest() {
        def ns = new Namespace("http://schemas.android.com/apk/res/android", "android")
        def xml = new XmlParser().parse(new InputStreamReader(new FileInputStream(manifestPath), "utf-8"))

        def application = xml.application[0]
        if (application) {
            def metaDataTags = application['meta-data']

            String rawApplicationName = application.attributes()[ns.name]
            metaDataTags.findAll {
                it.attributes()[ns.name].equals(TINKER_APPLICATION)
            }.each {
                it.parent().remove(it)
            }
            application.appendNode('meta-data', [(ns.name): TINKER_APPLICATION, (ns.value): rawApplicationName])
            application.attributes()[ns.name] = TINKER_APPLICATION_VALUE
            project.logger.error("tinkerpatch change application name from ${rawApplicationName} to ${TINKER_APPLICATION_VALUE}")

            def printer = new XmlNodePrinter(new PrintWriter(manifestPath, "utf-8"))
            printer.preserveWhitespace = true
            printer.print(xml)
        }
    }

打包出的apk中的AndroidManifest.xml文件基本是这样的

<application android:name="com.w4lle.onekeytinker.BootstrapApplication">
    ...
    <meta-data android:name="ONEKEY_TINKER_APPLICATION" android:value="com.w4lle.onekeytinker.App"/>
  </application>

其中的App是项目中原有的Application,BootstrapApplication是后期我们插入的Application。第一步完成。

另外说一句,这个Gradle插件的顺序应该是打包工具生成Manifest之后,Tinker相关Task之前。

运行时替换Application

这一步的主要工作也是分两步,第一就是解析Manifest文件,拿到realApplication(App)和BootstrapApplication;然后hook 系统完成替换。

InstantRun中的替换实现

public static void mon
keyPatchApplication(@Nullable Context context,
                                              @Nullable Application bootstrap,
                                              @Nullable Application realApplication) {
        try {
            // Find the ActivityThread instance for the current thread
            Class<?> activityThread = Class.forName("android.app.ActivityThread");
            Object currentActivityThread = getActivityThread(context, activityThread);

            // Find the mInitialApplication field of the ActivityThread to the real application
            Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication");
            mInitialApplication.setAccessible(true);
            Application initialApplication = (Application) mInitialApplication.get(currentActivityThread);
            if (realApplication != null && initialApplication == bootstrap) {
            //**2.替换掉ActivityThread.mInitialApplication**
                mInitialApplication.set(currentActivityThread, realApplication);
            }

            // Replace all instance of the stub application in ActivityThread#mAllApplications with the
            // real one
            if (realApplication != null) {
                Field mAllApplications = activityThread.getDeclaredField("mAllApplications");
                mAllApplications.setAccessible(true);
                List<Application> allApplications = (List<Application>) mAllApplications
                        .get(currentActivityThread);
                for (int i = 0; i < allApplications.size(); i++) {
                    if (allApplications.get(i) == bootstrap) {
                    //**1.替换掉ActivityThread.mAllApplications**
                        allApplications.set(i, realApplication);
                    }
                }
            }

            // Figure out how loaded APKs are stored.

            // API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know.
            Class<?> loadedApkClass;
            try {
                loadedApkClass = Class.forName("android.app.LoadedApk");
            } catch (ClassNotFoundException e) {
                loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");
            }
            Field mApplication = loadedApkClass.getDeclaredField("mApplication");
            mApplication.setAccessible(true);

            // 10 doesn't have this field, 14 does. Fortunately, there are not many Honeycomb devices
            // floating around.
            Field mLoadedApk = null;
            try {
                mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
            } catch (NoSuchFieldException e) {
                // According to testing, it's okay to ignore this.
            }

            // Enumerate all LoadedApk (or PackageInfo) fields in ActivityThread#mPackages and
            // ActivityThread#mResourcePackages and do two things:
            //   - Replace the Application instance in its mApplication field with the real one
            //   - Set Application#mLoadedApk to the found LoadedApk instance
            for (String fieldName : new String[]{"mPackages", "mResourcePackages"}) {
                Field field = activityThread.getDeclaredField(fieldName);
                field.setAccessible(true);
                Object value = field.get(currentActivityThread);

                for (Map.Entry<String, WeakReference<?>> entry :
                        ((Map<String, WeakReference<?>>) value).entrySet()) {
                    Object loadedApk = entry.getValue().get();
                    if (loadedApk == null) {
                        continue;
                    }

                    if (mApplication.get(loadedApk) == bootstrap) {
                        if (realApplication != null) {
                        //**3.替换掉mApplication**
                            mApplication.set(loadedApk, realApplication);
                        }
                        
                        if (realApplication != null && mLoadedApk != null) {
                        //**4.替换掉mLoadedApk**
                            mLoadedApk.set(realApplication, loadedApk);
                        }
                    }
                }
            }
        } catch (Throwable e) {
            throw new IllegalStateException(e);
        }
    }

主要做了两件事:

  1. 替换Application
    • baseContext.mPackageInfo.mApplication 代码3处
    • baseContext.mPackageInfo.mActivityThread.mInitialApplication 代码2处
    • baseContext.mPackageInfo.mActivityThread.mAllApplications 代码1处
  2. 替换mLoadedApk对象,代码4处

详细请查看 从Instant run谈Android替换Application和动态加载机制

做完上面这两步这样就可以实现一键接入了。

兼容性

在上篇文章中我们提到,由于该方案大量hook系统api,在国内Android碎片化如此严重的市场环境下,该方案兼容性有一些问题,大概有 1/1w的概率会出现替换失败的问题,如果替换失败,那么在系统中运行的Application还是BootstrapApplication,而我们App中的Application已经没有了Application的生命周期和作用。

所以我们要在失败catch中调用下Application的生命周期方法以保证程序能够正常初始化启动起来。

try {
  ...
} catch {
  e = true;
  realApplication.onCreate();
}

public void onConfiguration
Changed(Configuration paramConfiguration)
{
  if (e && realApplication != null) {
    realApplication.onConfiguration
Changed(paramConfiguration);
    return;
  }
  super.onConfiguration
Changed(paramConfiguration);
}

public void onLowMemory()
{
  if (e && realApplication != null) {
    realApplication.onLowMemory();
    return;
  }
  super.onLowMemory();
}

@TargetApi(14)
public void onTrimMemory(int paramInt)
{
  if (e && realApplication != null) {
    realApplication.onTrimMemory(paramInt);
    return;
  }
  super.onTrimMemory(paramInt);
}

public void onTerminate()
{
  if (e && realApplication != null) {
    realApplication.onTerminate();
    return;
  }
  super.onTerminate();
}

这么做虽然能保证App能启动,但是实际上还会有隐性问题存在。比如App中有如下代码 ((App) getApplication()).xxx(); ,那么在替换失败的情况下可能就会崩了, 因为 getApplication() 得到的是BootstrapApplication,强转为 App 类型肯定就挂了。

总结

整体思路大概讲清楚了,虽然这种方案接入成本低,但是兼容性问题是个很麻烦的事情,说不定啥时候就崩了。推荐大家还是使用Tinker自有的接入方案。

 
标签: 安卓开发
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • 安卓中通知功能的具体实现
    安卓中通知功能的具体实现
    通知[Notification]是Android中比较有特色的功能,当某个应用程序希望给用户发出一些提示信息,而该应用程序又不在前台运行时,就可以借助通知实现。使用通知的步骤1、需要一个NotificationManager来获得NotificationManager manager = (NotificationManager
    02-05 安卓开发
  • Android view系统分析-setContentView
    Android view系统分析-setContentView
    第一天上班,列了一下今年要学习的东西。主要就是深入学习Android相关的系统源代码,夯实基础。对于学习Android系统源代码,也没什么大概,就从我们平常使用最基础的东西学起,也就是从view这个切入点开始学习Android的源码,在没分析源码之前,我们有的时候
    02-05 安卓开发
  • 如何进行网络视频截图/获取视频的缩略图
    如何进行网络视频截图/获取视频的缩略图
    小编导读:获取视频的缩略图,截图正在播放的视频某一帧,是在音视频开发中,常遇到的问题。本文是主要用于点播中截图视频,同时还可以获取点播视频的缩略图进行显示,留下一个问题,如下图所示, 如果要获取直播中节目视频缩略图,该怎么做呢?(ps:直播是直
  • Android NDK 层发起 HTTP 请求的问题及解决
    Android NDK 层发起 HTTP 请求的问题及解决
    前言新的一年,大家新年快乐~~鸡年大吉!本次给大家带来何老师的最新文章~虽然何老师还在过节,但依然放心不下广大开发者,在此佳节还未结束之际,给大家带来最新的技术分享~ 事件的起因不说了,总之是需要实现一个 NDK 层的网络请求。为了多端适用,还是选择
  • Android插件化(六): OpenAtlasの改写aapt以防止资源ID冲突
    Android插件化(六): OpenAtlasの改写aapt以防
    引言Android应用程序的编译中,负责资源打包的是aapt,如果不对打包后的资源ID进行控制,就会导致插件中的资源ID冲突。所以,我们需要改写aapt的源码,以达到通过某种方式传递资源ID的Package ID,通过aapt打包时获取到这个Package ID并且应用才插件资源的命名
    02-05 安卓开发
  • Android架构(一)MVP架构在Android中的实践
    Android架构(一)MVP架构在Android中的实践
    为什么要重视程序的架构设计 对程序进行架构设计的原因,归根结底是为了 提高生产力 。通过设计是程序模块化,做到模块内部的 高聚合 和模块之间的 低耦合 (如依赖注入就是低耦合的集中体现)。 这样做的好处是使得程序开发过程中,开发人员主需要专注于一点,
    02-05 安卓开发
  • 安卓逆向系列教程 4.2 分析锁机软件
    安卓逆向系列教程 4.2 分析锁机软件
    安卓逆向系列教程 4.2 分析锁机软件 作者: 飞龙 这个教程中我们要分析一个锁机软件。像这种软件都比较简单,完全可以顺着入口看下去,但我这里还是用关键点来定位。首先这个软件的截图是这样,进入这个界面之后,除非退出模拟器,否则没办法回到桌面。上面那
    02-05 安卓开发
  • Android插件化(二):OpenAtlas插件安装过程分析
    Android插件化(二):OpenAtlas插件安装过程分析
    在前一篇博客 Android插件化(一):OpenAtlas架构以及实现原理概要 中,我们对应Android插件化存在的问题,实现原理,以及目前的实现方案进行了简单的叙述。从这篇开始,我们要深入到OpenAtlas的源码中进行插件安装过程的分析。 插件的安装分为3种:宿主启动时立
    02-05 安卓开发
  • [译] Android API 指南
    [译] Android API 指南
    众所周知,Android开发者有中文网站了,API 指南一眼看去最左侧的菜单都是中文,然而点进去内容还是很多是英文,并没有全部翻译,我这里整理了API 指南的目录,便于查看,如果之前还没有通读,现在可以好好看一遍。注意,如果标题带有英文,说明官方还没有翻
  • 使用FileProvider解决file:// URI引起的FileUriExposedException
    使用FileProvider解决file:// URI引起的FileUri
    问题以下是一段简单的代码,它调用系统的相机app来拍摄照片:void takePhoto(String cameraPhotoPath) {File cameraPhoto = new File(cameraPhotoPath);Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);takePhotoIntent.putExtra(Medi
    02-05 安卓开发
点击排行