Android插件化 Hook方式解决DialogFragment旋转问题

前言:

android插件化是这几年比较流行的技术,可以实现热更新,可以动态某些某块工程。

使用插件化开发项目的时候,插件项目中创建了一个DialogFramgent子类。
当屏幕旋转后,重新创建fragment,报错:

Caused by: android.app.Fragment$InstantiationException: Unable to instantiate fragment  com.xingen.plugin.dialog.MessageDialogFragment:  
make sure class name exists, is public, and has an empty constructor that is public
        at android.app.Fragment.instantiate(Fragment.java:618)
        at android.app.FragmentState.instantiate(Fragment.java:104)
        at android.app.FragmentManagerImpl.restoreAllState(FragmentManager.java:1777)
        at android.app.Activity.onCreate(Activity.java:953)
        at com.matchvs.union.ad.demo.MainActivity.onCreate(MainActivity.java:17)

源码探究原因


先从Activity.onCreate(),开始看起。

 protected void onCreate(@Nullable Bundle savedInstanceState) {

      if (savedInstanceState != null) {

            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
            mFragments.restoreAllState(p, mLastNonConfigurationInstances != null
                    ? mLastNonConfigurationInstances.fragments : null);
        }
        mFragments.dispatchCreate();
 }

接下来,源码走向到,FragmentController.restoreAllState():

public void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {
        mHost.mFragmentManager.restoreAllState(state, nonConfig);
}

接下来,源码走向到,FragmentManager.restoreAllState():

void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {
   //.....
        for (int i=0; i<fms.mActive.length; i++) {
            FragmentState fs = fms.mActive[i];
            if (fs != null) {
                FragmentManagerNonConfig childNonConfig = null;
                if (childNonConfigs != null && i < childNonConfigs.size()) {
                    childNonConfig = childNonConfigs.get(i);
                }
                Fragment f = fs.instantiate(mHost, mContainer, mParent, childNonConfig);

            }
        }
     //.....
}

接下来,源码走向到,FragmentState.instantiate:

  public Fragment instantiate(FragmentHostCallback host, FragmentContainer container,
            Fragment parent, FragmentManagerNonConfig childNonConfig) {
        if (mInstance == null) {
           //......
            if (container != null) {
                mInstance = container.instantiate(context, mClassName, mArguments);
            } else {
                mInstance = Fragment.instantiate(context, mClassName, mArguments);
            }

            //......
        }
        mInstance.mChildNonConfig = childNonConfig;
        return mInstance;
    }

最终锁定在,fragment.newInstance():

    public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
        try {
            Class<?> clazz = sClassMap.get(fname);
            if (clazz == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = context.getClassLoader().loadClass(fname);
                if (!Fragment.class.isAssignableFrom(clazz)) {
                    throw new InstantiationException("Trying to instantiate a class " + fname
                            + " that is not a Fragment", new ClassCastException());
                }
                sClassMap.put(fname, clazz);
            }
            Fragment f = (Fragment) clazz.getConstructor().newInstance();
            if (args != null) {
                args.setClassLoader(f.getClass().getClassLoader());
                f.setArguments(args);
            }
            return f;
        } catch (ClassNotFoundException e) {
            throw new InstantiationException("Unable to instantiate fragment " + fname
                    + ": make sure class name exists, is public, and has an"
                    + " empty constructor that is public", e);
        } catch (java.lang.InstantiationException e) {
            throw new InstantiationException("Unable to instantiate fragment " + fname
                    + ": make sure class name exists, is public, and has an"
                    + " empty constructor that is public", e);
        } catch (IllegalAccessException e) {
            throw new InstantiationException("Unable to instantiate fragment " + fname
                    + ": make sure class name exists, is public, and has an"
                    + " empty constructor that is public", e);
        } catch (NoSuchMethodException e) {
            throw new InstantiationException("Unable to instantiate fragment " + fname
                    + ": could not find Fragment constructor", e);
        } catch (InvocationTargetException e) {
            throw new InstantiationException("Unable to instantiate fragment " + fname
                    + ": calling Fragment constructor caused an exception", e);
        }
    }

通过查看源码可知,Activity在销毁重建时候,通过反射创建Fragment对象。因此,编写Fragment子类的时候,最好创建一个默认的构造方法。

在插件项目中,DialogFramgent子类添加默认的构造方法:

public class MessageDialogFragment extends DialogFragment {
    public static final String TAG=MessageDialogFragment.class.getSimpleName();
    public MessageDialogFragment (){}
}

满心欢喜的运行项目,依旧报错同样的错误。这时候,你就想到fragment.newInstance()源码中的ClassLoader加载不到,找不到插件中的MessageDialogFragment。

这时候,也许会想这是一个无解的bug,但好在,天无绝人之路,出现了Hook这种黑科技。

Android Hook大法


思路:

Hook一个插件的ClassLoader替代宿主的ClassLoader。

查找Hook点:

众所周知,Context的实现操作逻辑在ContextIml中,查看其获取ClassLoader的方法:

/**
 * Common implementation of Context API, which provides the base
 * context object for Activity and other application components.
 */
class ContextImpl extends Context {

    final @NonNull LoadedApk mPackageInfo;
    private @Nullable ClassLoader mClassLoader;
    @Override
    public ClassLoader getClassLoader() {
        return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader());
    }
}

从上面可知,classLoader可能来源于ContextImpl中classLoader或者LoadedApk的classLoader。

接下来,再找到一个,可以修改ContextImpl中classloadeer的hook点:

public class Instrumentation {
    /**
     * Perform calling of an activity's {@link Activity#onCreate}
     * method.  The default implementation simply calls through to that method.
     *
     * @param activity The activity being created.
     * @param icicle The previously frozen state (or null) to pass through to onCreate().
     */
    public void callActivityOnCreate(Activity activity, Bundle icicle) {
        prePerformCreate(activity);
        activity.performCreate(icicle);
        postPerformCreate(activity);
    }

}

开始编写Hook代码

1. 先创建一个HookManager,编写反射获取ActivityThread和替换Instrumentation对象

public class HookManager {
    public static void init(){
        try {
            //先获取当前的ActivityThread对象
            Class<?> activityThreadClass=Class.forName("android.app.ActivityThread");
            Method currentActivityThreadMethod= activityThreadClass.getDeclaredMethod("currentActivityThread");
            currentActivityThreadMethod.setAccessible(true);
            Object currentActivityThread=currentActivityThreadMethod.invoke(null);

            //拿到原始的Instrumentation
            Field mInstrumentationField=activityThreadClass.getDeclaredField("mInstrumentation");
            mInstrumentationField.setAccessible(true);
            Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
            //替换成Hook中的 Instrumentation
            InstrumentationFilter instrumentationFilter=new InstrumentationFilter();
            mInstrumentationField.set(currentActivityThread,instrumentationFilter);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

2. 在Instrumentation的callActivityOnCreate()中:

public class InstrumentationFilter extends Instrumentation {
    private static final String TAG=InstrumentationFilter.class.getSimpleName();
    @Override
    public void callActivityOnCreate(Activity activity, Bundle icicle) {
        try {
            Class<?> mClass = Class.forName("android.app.ContextImpl");
            Field[] fields = mClass.getDeclaredFields();
            Field mClassLoader = null;
            Field mPackageInfo = null;
            for (Field field : fields) {
                if (field.getName().equals("mClassLoader")) {
                    mClassLoader = field;

                }
                if (field.getName().equals("mPackageInfo")) {
                    mPackageInfo = field;
                }
            }
            if (mClassLoader != null) {
                mClassLoader.setAccessible(true);
                //将系统的加载Activity的ClassLoader替换成,插件的ClassLoader
                mClassLoader.set(activity.getBaseContext(), InstrumentationFilter.class.getClassLoader());
                return;
            }
            if (mPackageInfo != null) {
                mPackageInfo.setAccessible(true);
                Class<?> loadedApk=Class.forName("android.app.LoadedApk");
                 mClassLoader= loadedApk.getDeclaredField("mClassLoader");
                 mClassLoader.setAccessible(true);
                 //将系统中LoadedApk的ClassLoader
                 mClassLoader.set(mPackageInfo.get(activity.getBaseContext()),InstrumentationFilter.class.getClassLoader());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            super.callActivityOnCreate(activity, icicle);
        }
    }
}

反射替换掉ContextImpl中classLoader。

项目运行,效果如下
这里写图片描述

项目地址:https://github.com/13767004362/HookDemo

资源参考

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值