android插件化实践之路-1

很久以来都想尝试把项目插件化,做到跟h5一样无缝更新,也不会因为一些小bug而发紧急修复版本。但是基于种种众所周知的问题,一直不敢放在公司项目中来用。前几天同事看到360公布了方案,号称3年实际使用,心中又燃起了一些希望,这几天先下了滴滴的开源方案virtualapk,先摸索着试一下。

在此之前,先回顾下我们正常的apk逻辑。当系统启动时,会初始化PMS(PackageManageService),在这个过程中,会去遍历一些系统指定目录下的apk,并且将manifest中的信息读取并保存在内存中

一张图记录:

这里写图片描述

按照正常的思维方式,当我们启动一个activity时,系统应该会去找已经注册过的这个activity隶属的application的manifest属性列表,如果不存在这个类,就会报错;如果存在,就会通过一系列步骤后去回调他的Oncreate之类的方法来启动界面。

那么按照一般的插件化方案,被启动的那个activity通常是另外一个apk,存放在sdcard中,也就是系统压根不知道有这么个activity类存在,这里就存在第一个问题。

为了解惑这个问题,回头又看了一遍activity启动的过程:

按照一般的调用方式,当我们执行startActivity时,会实际调用之前分析的逻辑,见
http://blog.csdn.net/ouxie/article/details/55099146

在上面的这篇文章中可以看到,每次当我们启动一个新的Application,都会创建一个ActivityThread对象,
它负责管理一个application的主线程,管理activitys,broadcast等等。其主要成员如下:
这里写图片描述

这里主要看下mInstrumentation和mH这2个成员
前者是个Instrumentation对象。后者的成员函数列表如下:
这里写图片描述

在里面我们可以发现我们很熟悉的很多activity以及application的生命周期函数,这个类负责处理系统和我们应用之间的交互。此类的实体在handleBindApplication函数中有创建。

//如果应用的androidmanifest文件中有定义instrumentation,走这里。
if (data.instrumentationName != null) {
            InstrumentationInfo ii = null;
            try {
                ii = appContext.getPackageManager().
                    getInstrumentationInfo(data.instrumentationName, 0);
            } catch (PackageManager.NameNotFoundException e) {
            }
            if (ii == null) {
                throw new RuntimeException(
                    "Unable to find instrumentation info for: "
                    + data.instrumentationName);
            }

            mInstrumentationAppDir = ii.sourceDir;
            mInstrumentationAppLibraryDir = ii.nativeLibraryDir;
            mInstrumentationAppPackage = ii.packageName;
            mInstrumentedAppDir = data.info.getAppDir();
            mInstrumentedAppLibraryDir = data.info.getLibDir();

            ApplicationInfo instrApp = new ApplicationInfo();
            instrApp.packageName = ii.packageName;
            instrApp.sourceDir = ii.sourceDir;
            instrApp.publicSourceDir = ii.publicSourceDir;
            instrApp.dataDir = ii.dataDir;
            instrApp.nativeLibraryDir = ii.nativeLibraryDir;
            LoadedApk pi = getPackageInfo(instrApp, data.compatInfo,
                    appContext.getClassLoader(), false, true);
            ContextImpl instrContext = new ContextImpl();
            instrContext.init(pi, null, this);

            try {
                java.lang.ClassLoader cl = instrContext.getClassLoader();
                mInstrumentation = (Instrumentation)
                    cl.loadClass(data.instrumentationName.getClassName()).newInstance();
            } catch (Exception e) {
                throw new RuntimeException(
                    "Unable to instantiate instrumentation "
                    + data.instrumentationName + ": " + e.toString(), e);
            }

            mInstrumentation.init(this, instrContext, appContext,
                   new ComponentName(ii.packageName, ii.name), data.instrumentationWatcher,
                   data.instrumentationUiAutomationConnection);

            if (mProfiler.profileFile != null && !ii.handleProfiling
                    && mProfiler.profileFd == null) {
                mProfiler.handlingProfiling = true;
                File file = new File(mProfiler.profileFile);
                file.getParentFile().mkdirs();
                Debug.startMethodTracing(file.toString(), 8 * 1024 * 1024);
            }

        } else {
       //如果没自定义的话,走这里。
            mInstrumentation = new Instrumentation();
        }

当应用要启动一个activity时,会调用mInstrumentation的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++) {
                    final 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();
            int result = ActivityManagerNative.getDefault()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, null, options);
            checkStartActivityResult(result, intent);
        } catch (RemoteException e) {
        }
        return null;
    }

一路调用下去会通过handler回到上面说的第二个变量mH,关于这个变量可参考之前的文章。

case LAUNCH_ACTIVITY: {
                    /// M: enable profiling @{
                    if ( true == mEnableAppLaunchLog && !mIsUserBuild && false == mTraceEnabled ) {
                        try {
                            FileInputStream fprofsts_in = new FileInputStream("/proc/mtprof/status");
                            if ( fprofsts_in.read()== '3' ) {
                                Log.v(TAG, "start Profiling for empty process");
                                mTraceEnabled = true;
                                Debug.startMethodTracing("/data/data/applaunch"); //applaunch.trace
                            }
                        } catch (FileNotFoundException e) {
                            Slog.e(TAG, "mtprof entry can not be found", e);
                        } catch (java.io.IOException e) {
                            Slog.e(TAG, "mtprof entry open failed", e);
                        }
                    }
                    /// @}
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER | Trace.TRACE_TAG_PERF, "activityStart"); /// M: add TRACE_TAG_PERF for performance debug
                    ActivityClientRecord r = (ActivityClientRecord)msg.obj;

                    r.packageInfo = getPackageInfoNoCheck(
                            r.activityInfo.applicationInfo, r.compatInfo);
                    handleLaunchActivity(r, null);

在这个分支里,如果发现要起的activity并没有被记录,r.packageInfo就会为空。不过这里也没有说走不下去,还是可以无视地走下去。。。然后再看handleLaunchActivity(),直到performLaunchActivity()函数中,有以下一段:


        Activity activity = null;
        try {
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
            }
        }

这里的

activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
public Activity newActivity(Class<?> clazz, Context context, 
            IBinder token, Application application, Intent intent, ActivityInfo info, 
            CharSequence title, Activity parent, String id,
            Object lastNonConfigurationInstance) throws InstantiationException, 
            IllegalAccessException {
        Activity activity = (Activity)clazz.newInstance();
        ActivityThread aThread = null;
        activity.attach(context, aThread, this, token, application, intent,
                info, title, parent, id,
                (Activity.NonConfigurationInstances)lastNonConfigurationInstance,
                new Configuration());
        return activity;
    }

问题出来了,这个地方class应该是找不到的,也就初始化不了activity了。

那么滴滴的方案是怎么做的呢?来看下:

首先,当壳应用初始化时,完成以下步骤,hook部分变量们这里一共3个。

这里写图片描述

然后, 初始化完后,必须先load进存放于sdcard中的apk,并解析完需要的内容。
这里写图片描述

再者,Load完之后,我们正常使用startactivity函数时,会走如下过程:

调用vainstrumentation中的execStartActivity(),这里如果传入的是startActivyt(context, “com.child.test”)这样的intent,会替换为类似(com.didi.virtualapk.core.A.$1),如下代码:

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;
        }

我们在corelibrary工程中也确实能看到滴滴预留了十几个activity在他的manifest文件中。
这里写图片描述

然后调用系统的execStartActivity函数。由于VAInstrumentation被反射进去了,所以它的newActivity()会替换上述的系统函数,如下:


    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        try {
            cl.loadClass(className);
        } catch (ClassNotFoundException e) {
            LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
            String targetClassName = PluginUtil.getTargetActivity(intent);

            Log.i(TAG, String.format("newActivity[%s : %s]", className, targetClassName));

            if (targetClassName != null) {
                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);
    

这里的targetClassName就是我们原始要启动的“com.child.test”,再下面就是逐步把资源等替换成新的资源。从而达到启动的目的。最后一步应该是必须的,不过滴滴这里在前面execStartActivity的地方就进行了替换,应该是那之后的很多个步骤中某些地方会因为差找不到相应的activity注册会有问题的节奏。不过中间走过的代码实在太多了,不想看了。。。当然它的方案的复杂度远不止如此,这里只是先熟悉下,为接下来的工作做准备。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值