Android 插件式Android

搞懂插件化,看这一篇就够了

2023-02-21  本文已影响0人  尹学姐

背景

历史和现状

发展历史

现状

笼统的分类

免安装型

  • 宿主和插件单独编译。
  • 加载一个与主app无业务关系的独立apk,实现不安装即可使用功能。

自解耦型

  • 宿主和插件共同完成编译。
  • 偏向于在“组件化”的基础上,将“组件”从主app中剥离为“插件”。

插件化关注点

VirtualApk源码

VirtualApk作为一个开创性的插件化框架,源码非常具有借鉴意义。下面从VirtualApk源码的角度,从各个方面来讨论,如何实现一个插件化框架。

加载&管理插件

加载流程

PluginManager

public void loadPlugin(File apk) throws Exception {
    // ... 检查apk文件是否存在
    // 创建LoadedPlugin
    LoadedPlugin plugin = createLoadedPlugin(apk);
    
    // 缓存到mPlugins map 中
    this.mPlugins.put(plugin.getPackageName(), plugin);
    synchronized (mCallbacks) {
        for (int i = 0; i < mCallbacks.size(); i++) {
            // 插件Load成功回调
            mCallbacks.get(i).onAddedLoadedPlugin(plugin);
        }
    }
}

创建LoadedPlugin大致流程:

public LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws Exception {

    // 反射调用PackageParser.parsePackage解析apk,获取Package对象
    this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
    this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData;
   
    // 构造PackageInfo对象
    this.mPackageInfo = new PackageInfo();
    // ... 将package中内容复制到PackageInfo中
    
    // 构造PluginPacakgeManager对象
    this.mPackageManager = createPluginPackageManager();
    
    // 构造PluginContext对象
    this.mPluginContext = createPluginContext(null);
    
    // 构造Resources对象
    this.mResources = createResources(context, getPackageName(), apk);
    
    // 构造ClassLoader
    this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());
    
    // 拷贝so库。将CPU-ABI对应的SO拷贝到宿主的目录下。
    tryToCopyNativeLib(apk);

    // 缓存Manifest中的Activities/Services/Content Provider
    // ...
   
    // 将静态广播转为动态 
    // ...
   
    // 实例化插件的Application,并调用onCreate
    invokeApplication();
}

具体做法

parsePacakge

// 不同的Android版本,hook的方式不一样
static final PackageParser.Package parsePackage(Context context, File apk, int flags) throws Throwable {
    PackageParser parser = new PackageParser();
    // 从apk中解析出Package,包含packageName/versionCode/versionName/四大组件等信息
    PackageParser.Package pkg = parser.parsePackage(apk, flags);
    // 通过collectCertificates方法获取应用的签名信息mSignatures
    Reflector.with(parser)
        .method("collectCertificates", PackageParser.Package.class, int.class)
        .call(pkg, flags);
    return pkg;
}

创建插件的PackageManager

// 创建PluginPackageManager
protected class PluginPackageManager extends PackageManager {
    @Override
    public PackageInfo getPackageInfo(String packageName, int flags) throws NameNotFoundException {
        // 使用包名从PluginManager中获取插件LoadedPlugin
        LoadedPlugin plugin = mPluginManager.getLoadedPlugin(packageName);
        if (null != plugin) {
            // 获取插件PackageInfo
            return plugin.mPackageInfo;
        }
        // 如果没找到,则使用宿主的PackageInfo
        return this.mHostPackageManager.getPackageInfo(packageName, flags);
    }
    
    @Override
    public ActivityInfo getActivityInfo(ComponentName component, int flags) throws NameNotFoundException {
        LoadedPlugin plugin = mPluginManager.getLoadedPlugin(component);
        if (null != plugin) {
            // 从LoadedPlugin获取ActivityInfo
            return plugin.mActivityInfos.get(component);
        }
        return this.mHostPackageManager.getActivityInfo(component, flags);
    }
    
    // ...
}

创建插件的Context

class PluginContext extends ContextWrapper{

    private final LoadedPlugin mPlugin;

    public PluginContext(LoadedPlugin plugin) {
        super(plugin.getPluginManager().getHostContext());
        this.mPlugin = plugin;
    }
    
    @Override
    public ClassLoader getClassLoader() {
        // 获取插件ClassLoader
        return this.mPlugin.getClassLoader();
    }
    
    @Override
    public PackageManager getPackageManager() {
        // 获取插件PackageManager
        return this.mPlugin.getPackageManager();
    }

    @Override
    public Resources getResources() {
        // 获取插件的Resources
        return this.mPlugin.getResources();
    }

    @Override
    public AssetManager getAssets() {
        // 获取插件的AssetManager
        return this.mPlugin.getAssets();
    }

    @Override
    public void startActivity(Intent intent) {
        // 启动插件的activity
        ComponentsHandler componentsHandler = mPlugin.getPluginManager().getComponentsHandler();
        componentsHandler.transformIntentToExplicitAsNeeded(intent);
        super.startActivity(intent);
    }
}

创建插件的PackageManger和Context的用处,是为了在后续的使用中,更方便得使用插件的ClassLoader,Resources等资源。比如创建Activity后Hook掉Context。

Resources

打包aapt做了什么

image

运行时获取资源

VirtualApk提供两种资源处理方式

protected Resources createResources(Context context, String packageName, File apk) throws Exception {
    if (Constants.COMBINE_RESOURCES) {
        // 合并宿主和插件资源
        return ResourcesManager.createResources(context, packageName, apk);
    } else {
        Resources hostResources = context.getResources();
        // 新创建一个assetmanager
        AssetManager assetManager = createAssetManager(context, apk);
        // 使用新的assetmanager和宿主的屏幕参数信息(DPI、屏幕宽高等),初始化resource。
        return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
    }
}
1. 资源独立
protected AssetManager createAssetManager(Context context, File apk) throws Exception {
    AssetManager am = AssetManager.class.newInstance();
    // 调用addAssetPath将插件apk的资源加入到assetManager
    Reflector.with(am).method("addAssetPath", String.class).call(apk.getAbsolutePath());
    return am;
}
2. 资源合并

ResourceManager.createResources

public static synchronized Resources createResources(Context hostContext, String packageName, File apk) throws Exception {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        // N之后可以直接hook mSplitResDirs,原理一样
        return createResourcesForN(hostContext, packageName, apk);
    }
    
    // 创建合并资源后的newResources
    Resources resources = ResourcesManager.createResourcesSimple(hostContext, apk.getAbsolutePath());
    // 替换宿主的mResources
    ResourcesManager.hookResources(hostContext, resources);
    return resources;
}

ResourceManager.createResourcesSimple

private static Resources createResourcesSimple(Context hostContext, String apk) throws Exception {
    Resources hostResources = hostContext.getResources();
    Reflector reflector = Reflector.on(AssetManager.class).method("addAssetPath", String.class);
    // 获取宿主的AssetManager
    AssetManager assetManager = hostResources.getAssets();
    reflector.bind(assetManager);
    // 调用addAssetPath添加插件资源
    final int cookie2 = reflector.call(apk);
    // 添加所有插件的资源
    List<LoadedPlugin> pluginList = PluginManager.getInstance(hostContext).getAllLoadedPlugins();
    for (LoadedPlugin plugin : pluginList) {
        final int cookie3 = reflector.call(plugin.getLocation());
    }
    // 创建newResources
    Resources newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());an
    // 更新所有插件的Resources
    for (LoadedPlugin plugin : pluginList) {
        plugin.updateResources(newResources);
    }
    
    return newResources;
}

ResourceManager.hookResources

// 用newResources替换宿主的Resources
public static void hookResources(Context base, Resources resources) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        return;
    }
    try {
        // 替换宿主context中的mReources
        Reflector reflector = Reflector.with(base);
        reflector.field("mResources").set(resources);
        // 替换宿主PackageInfo中的mResources
        Object loadedApk = reflector.field("mPackageInfo").get();
        Reflector.with(loadedApk).field("mResourtces").set(resources);

        Object activityThread = ActivityThread.currentActivityThread();
        Object resManager;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            resManager = android.app.ResourcesManager.getInstance();
        } else {
            resManager = Reflector.with(activityThread).field("mResourcesManager").get();
        }
        // 替换ResourceManager中的resource缓存,将newResources的弱引用放入map中,新创建的context会使用newResources
        Map<Object, WeakReference<Resources>> map = Reflector.with(resManager).field("mActiveResources").get();
        Object key = map.keySet().iterator().next();
        map.put(key, new WeakReference<>(resources));
    } catch (Exception e) {
        Log.w(TAG, e);
    }
}

创建context的时候,会调用ResourceManager.getTopLevelResources()来获取Resources,所有context里面的资源都来自于此处。

public Resources getTopLevelResources(String resDir, int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {  
    final float scale = compatInfo.applicationScale;  
    // 创建key
    ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token);  
    Resources r;  
    synchronized (this) {  
        // 是否在mActiveResources存在
        WeakReference<Resources> wr = mActiveResources.get(key);  
        r = wr != null ? wr.get() : null;  
        if (r != null && r.getAssets().isUpToDate()) {  
            return r;  
        }  
    }  
  
    // 创建Resources
    AssetManager assets = new AssetManager();  
    r = new Resources(assets, dm, config, compatInfo, token);  
  
    synchronized (this) {  
        WeakReference<Resources> wr = mActiveResources.get(key);  
        // 将新创建的Resources的弱引用存入ActiveResources
        mActiveResources.put(key, new WeakReference<Resources>(r));
        return r;  
    }  
}  

合并资源的问题

1. 资源id重复

资源打包时,会对res目录下资源文件分配一个唯一Id。


image

解决方法:
重写AAPT命令,在插件apk打包过程中,通过指定资源id的前缀PP字段,来保证宿主和插件的资源id永远不会冲突。

2. 宿主升级

宿主升级,旧版本插件配新版本宿主,需要保证原来插件调用的资源id不能改变,否则宿主升级后,加载的插件还是拿取的旧版本资源id,会导致资源找不到和错乱情况。
所以,宿主中被插件使用的资源要保证:

ClassLoader

类加载源码

loadClass

// 双亲委派机制
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
        // 首先,检查类是否已被加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // parent未找到类
            }

            if (c == null) {
                // 自己去加载类
                c = findClass(name);
            }
        }
        return c;
}

findClass

protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    //通过DexPathList查找类
    Class c = pathList.findClass(name, suppressedExceptions);
    // ...
    return c;
}
public Class findClass(String name, List<Throwable> suppressed) {
    // 遍历dexElements查找
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;

        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    return null;
}

DexClassLoader & PathClassLoader 区别

BaseDexClassLoader构造函数:

// dex文件路径/odex文件输出目录/动态库路径/parent classloader
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
        String librarySearchPath, ClassLoader parent) {
    this(dexPath, librarySearchPath, parent, null, false);
}

PathClassLoader构造函数:

public PathClassLoader(String dexPath, ClassLoader parent) {
    super(dexPath, null, null, parent);
}
    
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
    super(dexPath, null, librarySearchPath, parent);
}

DexClassLoader构造函数

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

多ClassLoader & 单ClassLoader

多ClassLoader

多 ClassLoader 的方案,还可以细分为两种:一种是每个自定义 ClassLoader 的 parent 为当前宿主应用的 ClassLoader 即是 PathClassLoader,这种方案将宿主视为运行环境,插件需依赖宿主运行,插件之间互相隔离,如下图:


image

一种是每个自定义 ClassLoader 的 parent 为 BootClassLoader,这种方案类似原生应用隔离的方案,宿主与插件、插件与插件互相独立,如下图:


image
单ClassLoader

这种方案是委托给应用的PathClassLoader加载.dex,宿主与插件共享同一个 ClassLoader。 BaseDexClassLoader 在构造时生创建一个DexPathList,而DexPathList内部有一个叫做dexElements数组,我们要做的就是将 dex 文件插入到这个dexElements数组中,在 PathClassLoader 中查找类时,就会遍历这个数组中 DexFile 的信息,完成插件类的加载。

VirtualApk插件ClassLoader的创建

protected ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) throws Exception {
    File dexOutputDir = getDir(context, Constants.OPTIMIZE_DIR);
    String dexOutputPath = dexOutputDir.getAbsolutePath();
    // dex文件路径 优化后的odex文件路径 动态库路径 宿主的classloader
    DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);

    if (Constants.COMBINE_CLASSLOADER) {
        // 合并到宿主, 宿主能访问插件的类
        DexUtil.insertDex(loader, parent, libsDir);
    }

    return loader;
}
合并DexElements

DexUtils.insertDex

public static void insertDex(DexClassLoader dexClassLoader, ClassLoader baseClassLoader, File nativeLibsDir) throws Exception {
    // 宿主的dexElements
    Object baseDexElements = getDexElements(getPathList(baseClassLoader));
    // 插件dexElements
    Object newDexElements = getDexElements(getPathList(dexClassLoader));
    // 合并后,宿主的dexElements在插件前面
    Object allDexElements = combineArray(baseDexElements, newDexElements);
    // 将合并后的dexElements设置到宿主PathClassloader中
    Object pathList = getPathList(baseClassLoader);
    Reflector.with(pathList).field("dexElements").set(allDexElements);
    
    // 插入so库
    insertNativeLibrary(dexClassLoader, baseClassLoader, nativeLibsDir);
}
总结

四大组件

四大组件没有在宿主的Manifest中注册,所以需要做一些Hook操作来绕过系统的检查。

Activity启动

几个问题:

Hook流程

Activity启动流程
image
去程Hook
  // 动态代理
  public static void hookActivityManagerService() throws Reflector.ReflectedException{
    Object gDefaultObj = null;
    // API 29 及以后hook android.app.ActivityTaskManager.IActivityTaskManagerSingleton
    // API 26 及以后hook android.app.ActivityManager.IActivityManagerSingleton
    // API 25 以前hook android.app.ActivityManagerNative.gDefault
    if(Build.VERSION.SDK_INT >= 29){
      gDefaultObj = Reflector.on("android.app.ActivityTaskManager").field("IActivityTaskManagerSingleton").get();
    }else if(Build.VERSION.SDK_INT >= 26){
      gDefaultObj = Reflector.on("android.app.ActivityManager").field("IActivityManagerSingleton").get();
    }else{
      gDefaultObj = Reflector.on("android.app.ActivityManagerNative").field("gDefault").get();
    }
    Object amsObj = Reflector.with(gDefaultObj).field("mInstance").get();
    // 本地的类加载器;
    // 代理类的对象所继承的接口(用Class数组表示,支持多个接口)
    // 代理类的实际逻辑,封装在new出来的InvocationHandler内
    Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
        amsObj.getClass().getInterfaces(), new IActivityManagerHandler(amsObj));
    Reflector.with(gDefaultObj).field("mInstance").set(proxy);
  }

IActivityManagerHandler

public class IActivityManagerHandler implements InvocationHandler {
    Object mBase;
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Log.d(TAG, "invoke " + method.getName());
        // 如果是启动Activity,替换Intent
        if("startActivity".equals(method.getName())){
          hookStartActivity(args);
          return method.invoke(mBase, args);
        }else if("startService".equals(method.getName())){
          // 将所有的操作进行拦截,都改为startService,然后统一在onStartCommand中分发
        }
        return method.invoke(mBase, args);
    }
}

替换Activity

// 替换为占位Activity
private void hookStartActivity(Object[] args){
    int index = getIntentIndex(args);
    Intent intent = (Intent) args[index];
    
    // 将插件的隐式intent转化为显式intent,host的intent不变
    ComponentName component = intent.getComponent();
    // component为空,且非host
    if(component == null){
      // host resolveinfo 为null
      ResolveInfo info = mPluginManager.resolveActivity(intent);
      if(info != null && info.activityInfo != null){
        component = new ComponentName(info.activityInfo.packageName, info.activityInfo.name);
        intent.setComponent(component);
      }
    }
    
    // Component不为空,且非host
    if(intent.getComponent() != null
        && !intent.getComponent().getPackageName().equals(mPluginManager.getHostContext().getPackageName())){
      Intent newIntent = new Intent();
      String stubPackage = mPluginManager.getHostContext().getPackageName();
      // 占位Activity的名称
      ComponentName componentName = new ComponentName(stubPackage,
          mPluginManager.getComponentsHandler().getStubActivityClass(intent));
      newIntent.setComponent(componentName);
    
      // 将之前的intent存起来
      newIntent.putExtra(Constants.KEY_IS_PLUGIN, true);
      newIntent.putExtra(Constants.EXTRA_TARGET_INTENT, intent);
      args[index] = newIntent;
      Log.d(TAG, "hook succeed");
    }
}
回程Hook
 // Hook ActivityThread 中的 mH
 public void hookActivityThreadCallback() throws Exception {
    ActivityThread activityThread = ActivityThread.currentActivityThread();
    Handler handler = Reflector.with(activityThread).field("mH").get();
    Reflector.with(handler).field("mCallback").set(new ActivityThreadHandlerCallback(handler));
 }

ActivityThreadHandlerCallback

@Override
public boolean handleMessage(@NonNull Message msg) {
    Log.d(TAG, "handle Message " + msg.what);
    if(what == 0){
      try{
        // Hook获取到EXECUTE_TRANSACTION的值
        ActivityThread activityThread = ActivityThread.currentActivityThread();
        Handler handler = Reflector.with(activityThread).field("mH").get();
        what = Reflector.with(handler).field("EXECUTE_TRANSACTION").get();
      }catch (Reflector.ReflectedException e){
        e.printStackTrace();
        what = EXECUTE_TRANSACTION;
      }
    }
    // 如果是EXECUTE_TRANSACTION
    if(msg.what == what){
      handleLaunchActivity(msg);
    }
    return false;
}
private void handleLaunchActivity(Message msg){
    try{
      List list = Reflector.with(msg.obj).field("mActivityCallbacks").get();
      if(list == null || list.isEmpty()) return;
      Class<?> launchActivityItemClz = Class.forName("android.app.servertransaction.LaunchActivityItem");
      if(launchActivityItemClz.isInstance(list.get(0))) {
        // 从LaunchActivityItem中获取到待启动的intent
        Intent intent = Reflector.with(list.get(0)).field("mIntent").get();
        // 待启动的intent中保存的target,就是插件activity的信息
        Intent target = intent.getParcelableExtra(Constants.EXTRA_TARGET_INTENT);
        if(target != null){
          // 替换回原来的activity
          intent.setComponent(target.getComponent());
        }
      }
    }catch (Reflector.ReflectedException e){
      e.printStackTrace();
    }catch (ClassNotFoundException e){
      e.printStackTrace();
    }
}
创建插件Activity

VAInstrumentation

@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent)
  throws ClassNotFoundException, IllegalAccessException, InstantiationException {
    try {
      // 宿主的ClassLoader
      cl.loadClass(className);
    } catch (ClassNotFoundException e) {
      ComponentName component = intent.getComponent();
    
      if (component != null) {
        String targetClassName = component.getClassName();
        LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(component.getPackageName());
        if (loadedPlugin != null) {
          // 使用插件的classLoader加载
          Activity activity =
              mBase.newActivity(loadedPlugin.getClassLoader(), targetClassName, intent);
          return activity;
        }
      }
    }
    return super.newActivity(cl, className, intent);
}

替换Activity中的Resources & Context

@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
    injectActivity(activity);
    mBase.callActivityOnCreate(activity, icicle);
}
protected void injectActivity(Activity activity) {
    final Intent intent = activity.getIntent();
    if (PluginUtil.isIntentFromPlugin(intent)) {
        Context base = activity.getBaseContext();
        try {
            LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
             // 替换插件Activity context的mResources
            Reflector.with(base).field("mResources").set(plugin.getResources());
            Reflector reflector = Reflector.with(activity);
            // 替换插件Activity的Context
            reflector.field("mBase").set(plugin.createPluginContext(activity.getBaseContext()));
            // 替换插件Activity的Application
            reflector.field("mApplication").set(plugin.getApplication());

            // set screenOrientation
            ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
            if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
                activity.setRequestedOrientation(activityInfo.screenOrientation);
            }

            // for native activity
            ComponentName component = PluginUtil.getComponent(intent);
            Intent wrapperIntent = new Intent(intent);
            wrapperIntent.setClassName(component.getPackageName(), component.getClassName());
            activity.setIntent(wrapperIntent);
            
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }
}co
LaunchMode

在Manifest中注册各种LaunchMode的Activity,根据LaunchMode来按顺序匹配到不同的StubActivity

image

BroadcastReceiver

// BroadcastReceiver静态转动态,将插件的静态receiver动态注册到host中
Map<ComponentName, ActivityInfo> receivers = new HashMap<>();
for(PackageParser.Activity receiver : this.mPackage.receivers){
  receivers.put(receiver.getComponentName(), receiver.info);
  BroadcastReceiver br = BroadcastReceiver.class.cast(
      // 用插件的classloader去加载
      getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
      for(PackageParser.ActivityIntentInfo aii : receiver.intents){
        mHostContext.registerReceiver(br, aii);
      }
}

总结

优点

缺点

Qigsaw

概况

依赖于Dynamic Features & Split Apks

image

安装split apks的方法

与其他框架的区别

四大组件

image

ClassLoader

image

Resources

/**
 * Activities of base apk which would load split's fragments or resources.
 */
baseContainerActivities = [

    // gamecenterplugin
    "com.yxcorp.gifshow.gamecenter.cloudgame.ZtGameCloudPlayActivity"
]

多进程问题

子进程需要初始化qigsaw,但是子进程未加载过插件Split Apks。

qigsaw的解决方案:

private Class<?> onClassNotFound(String name) {
    // 加载所有已安装的splits
    SplitLoadManagerService.getInstance().loadInstalledSplits();
    ret = findClassInSplits(name);
    if (ret != null) {
        SplitLog.i(TAG, "Class %s is found in Splits after loading all installed splits.", name);
        return ret;
    }
    return null;
}

插件更新

总结

优点

缺点

上一篇下一篇

猜你喜欢

热点阅读