Flutter源码系列之《一》Flutter的热更新探索(上)

转载请注明出处:https://blog.csdn.net/llew2011/article/details/104075796

在Google发布Flutter之后我一直对它保持很高的热情,尤其是Flutter支持热更新,但在1.7.8版本之后由于某些原因Google屏蔽了热更新功能,这着实让让人失落……于是抽了点时间研究了一下Flutter的加载过程,目的是找到可以实现热更新的方式,在阅读文章之前需要小伙伴们对Flutter的编译模式有一定了解,目前我使用的Flutter版本为dev分支的1.14.0,Dart版本为2.8.0,如下所示:
在这里插入图片描述
要实现Flutter的热更新功能就要先弄清楚Flutter的加载流程,比如Flutter引擎什么时机加载,引擎加载完毕后libapp.so文件什么时机加载等,因此我们先分析一下Flutter的加载过程。

首先新建Flutter项目flutter_hotfix,然后运行该项目,如下所示:
在这里插入图片描述
项目启动后运行如上所示,页面上展示了一个文案和计数器数值,点击+按钮计数器的值就会自增。接下来我们看下在Manifest中配置的启动信息,打开AndroidManifext.xml文件,如下:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.llew.flutter.flutter_hotfix">
    <!-- io.flutter.app.FlutterApplication is an android.app.Application that
         calls FlutterMain.startInitialization(this); in its onCreate method.
         In most cases you can leave this as-is, but you if you want to provide
         additional functionality it is fine to subclass or reimplement
         FlutterApplication and put your custom class here. -->
    <application
        android:name="io.flutter.app.FlutterApplication"
        android:label="flutter_hotfix"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>

Manifest默认配置了当前项目的Application为FlutterApplication,启动页面是MainActivity,另外配置的meta-data信息我们先不关注,我们先看FlutterApplication类(在flutter.jar包中),如下所示:

public class FlutterApplication extends Application {
    @Override
    @CallSuper
    public void onCreate() {
        super.onCreate();
        FlutterMain.startInitialization(this);
    }

    private Activity mCurrentActivity = null;
    // 省略set/get方法
}

FlutterApplication继承Application,它定义了Activity类型的mCurrentActivity属性并给其添加了set/get方法,另外FlutterApplication重写了onCreate()方法,在onCreate()方法中仅调用了FlutterMain的startInitialization()静态方法进行初始化操作,FlutterMain源码如下:

public class FlutterMain {

    /**
     * Starts initialization of the native system.
     * @param applicationContext The Android application context.
     */
    public static void startInitialization(@NonNull Context applicationContext) {
        if (isRunningInRobolectricTest) {
            return;
        }
        FlutterLoader.getInstance().startInitialization(applicationContext);
    }

    /**
     * Starts initialization of the native system.
     * <p>
     * This loads the Flutter engine's native library to enable subsequent JNI calls. This also
     * starts locating and unpacking Dart resources packaged in the app's APK.
     * <p>
     * Calling this method multiple times has no effect.
     *
     * @param applicationContext The Android application context.
     * @param settings Configuration settings.
     */
    public static void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) {
        if (isRunningInRobolectricTest) {
            return;
        }
        FlutterLoader.Settings newSettings = new FlutterLoader.Settings();
        newSettings.setLogTag(settings.getLogTag());
        FlutterLoader.getInstance().startInitialization(applicationContext, newSettings);
    }

    // 省略相关代码
}

FlutterMain定义了一系列静态方法,而这些方法内部又委托FlutterLoader实现具体功能,FlutterLoader看名字就知道是一个加载类,它可能是我们实现热更新的入口类。FlutterLoader对外提供了一个静态的getInstance()方法便于获取其实例(该方法是单例的),其startInitialization()源码如下:

public class FlutterLoader {
    private static final String TAG = "FlutterLoader";

  	// 省略部分属性

    private static FlutterLoader instance;

    /**
     * Returns a singleton {@code FlutterLoader} instance.
     * <p>
     * The returned instance loads Flutter native libraries in the standard way. A singleton object
     * is used instead of static methods to facilitate testing without actually running native
     * library linking.
     */
    @NonNull
    public static FlutterLoader getInstance() {
        if (instance == null) {
            instance = new FlutterLoader();
        }
        return instance;
    }

    /**
     * Starts initialization of the native system.
     * @param applicationContext The Android application context.
     */
    public void startInitialization(@NonNull Context applicationContext) {
        startInitialization(applicationContext, new Settings());
    }

    /**
     * Starts initialization of the native system.
     * <p>
     * This loads the Flutter engine's native library to enable subsequent JNI calls. This also
     * starts locating and unpacking Dart resources packaged in the app's APK.
     * <p>
     * Calling this method multiple times has no effect.
     *
     * @param applicationContext The Android application context.
     * @param settings Configuration settings.
     */
    public void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) {
      	// 允许多次调用
        // Do not run startInitialization more than once.
        if (this.settings != null) {
          return;
        }
      	// 只允许在主线程运行
        if (Looper.myLooper() != Looper.getMainLooper()) {
          throw new IllegalStateException("startInitialization must be called on the main thread");
        }

        this.settings = settings;

        long initStartTimestampMillis = SystemClock.uptimeMillis();
      	// 初始化配置信息
        initConfig(applicationContext);
        // 初始化资源信息
        initResources(applicationContext);
				// 加载Flutter引擎
        System.loadLibrary("flutter");

        VsyncWaiter
            .getInstance((WindowManager) applicationContext.getSystemService(Context.WINDOW_SERVICE))
            .init();

        // We record the initialization time using SystemClock because at the start of the
        // initialization we have not yet loaded the native library to call into dart_tools_api.h.
        // To get Timeline timestamp of the start of initialization we simply subtract the delta
        // from the Timeline timestamp at the current moment (the assumption is that the overhead
        // of the JNI call is negligible).
        long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis;
      	// 记录加载时间
        FlutterJNI.nativeRecordStartTimestamp(initTimeMillis);
    }

    /**
     * 根据AndroidManifest文件初始化相关配置信息
     * Initialize our Flutter config values by obtaining them from the
     * manifest XML file, falling back to default values.
     */
    private void initConfig(@NonNull Context applicationContext) {
        Bundle metadata = getApplicationInfo(applicationContext).metaData;

        // There isn't a `<meta-data>` tag as a direct child of `<application>` in
        // `AndroidManifest.xml`.
        if (metadata == null) {
            return;
        }

        aotSharedLibraryName = metadata.getString(PUBLIC_AOT_SHARED_LIBRARY_NAME, DEFAULT_AOT_SHARED_LIBRARY_NAME);
        flutterAssetsDir = metadata.getString(PUBLIC_FLUTTER_ASSETS_DIR_KEY, DEFAULT_FLUTTER_ASSETS_DIR);

        vmSnapshotData = metadata.getString(PUBLIC_VM_SNAPSHOT_DATA_KEY, DEFAULT_VM_SNAPSHOT_DATA);
        isolateSnapshotData = metadata.getString(PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY, DEFAULT_ISOLATE_SNAPSHOT_DATA);
    }

    /**
     * Extract assets out of the APK that need to be cached as uncompressed
     * files on disk.
     */
    private void initResources(@NonNull Context applicationContext) {
      	// 先清空资源
        new ResourceCleaner(applicationContext).start();
				// 只在DEBUG或者JIT_RELEASE模式下执行
        if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
            final String dataDirPath = PathUtils.getDataDirectory(applicationContext);
            final String packageName = applicationContext.getPackageName();
            final PackageManager packageManager = applicationContext.getPackageManager();
            final AssetManager assetManager = applicationContext.getResources().getAssets();
            resourceExtractor = new ResourceExtractor(dataDirPath, packageName, packageManager, assetManager);

            // In debug/JIT mode these assets will be written to disk and then
            // mapped into memory so they can be provided to the Dart VM.
            resourceExtractor
                .addResource(fullAssetPathFrom(vmSnapshotData))
                .addResource(fullAssetPathFrom(isolateSnapshotData))
                .addResource(fullAssetPathFrom(DEFAULT_KERNEL_BLOB));

            resourceExtractor.start();
        }
    }

    // 省略部分代码
}

FlutterLoader的startInitialization()方法核心就是初始化相关资源并加载Flutter引擎libflutter.so,该方法根据内部属性settings是否非空做个开关从而允许多次调用,然后调用initConfig()和initResources()方法进行相关初始化操作,需要注意的是initResources()方法内仅在DEBUG和JIT_RELEASE模式下才执行,也就是说在我们正式发布RELEASE版本的时候initResources()方法没有多大用处,然后执行System.loadLibrary(“flutter”)加载Flutter引擎,最后调用native方法记录Flutter引擎的加载时间。

到这里我们分析完了Flutter引擎的加载时机,然而我们Flutter项目打包出来的libapp.so还没有被加载,那它是什么时机加载的呢?我们继续往下分析代码,根据Manifest的配置,启动页面配置的是MainActivity,因此我们需要分析下MainActivity,它的源码如下所示:

public class MainActivity extends FlutterActivity {
    @Override
    public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine);
    }
}

MainActivity继承FlutterActivity,它仅重写了configureFlutterEngine()方法,一般情况下创建完Activity后会在其onCreate()方法中调用setContentView()为当前Activity添加视图,可MainActivity没有重写onCreate()方法也没setContentView()的调用并且项目运行后界面正常渲染,那只能说明它的父类FlutterActivity调用了setContentView()方法,我们看下FlutterActivity的源码,如下所示:

public class FlutterActivity extends Activity
    implements FlutterActivityAndFragmentDelegate.Host, LifecycleOwner {
  private static final String TAG = "FlutterActivity";

  // Delegate that runs all lifecycle and OS hook logic that is common between
  // FlutterActivity and FlutterFragment. See the FlutterActivityAndFragmentDelegate
  // implementation for details about why it exists.
  @VisibleForTesting
  protected FlutterActivityAndFragmentDelegate delegate;

  @NonNull
  private LifecycleRegistry lifecycle;

  public FlutterActivity() {
    lifecycle = new LifecycleRegistry(this);
  }

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
		// 省略代码
    super.onCreate(savedInstanceState);

    lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);

		// 初始化delegate实例并持有当前Activity
    delegate = new FlutterActivityAndFragmentDelegate(this);
    delegate.onAttach(this);
    delegate.onActivityCreated(savedInstanceState);

    configureWindowForTransparency();
    // 调用setContentView()方法为当前Activity添加视图
    setContentView(createFlutterView());
    configureStatusBarForFullscreenFlutterExperience();
  }

  @NonNull
  private View createFlutterView() {
    return delegate.onCreateView(
        null /* inflater */,
        null /* container */,
        null /* savedInstanceState */);
  }

  @Override
  protected void onStart() {
    super.onStart();
    lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START);
    // 处理生命周期
    delegate.onStart();
  }

  // 省略部分代码
}

FlutterActivity继承Activity并实现了Host和LifecycleOwner接口并定义了一个重量级属性delegate,delegate是FlutterActivityAndFragmentDelegate类型,它是FlutterActivity的大管家,FlutterActivity的众多回调都委托给了delegate,FlutterActivityAndFragmentDelegate构造方法如下所示:

final class FlutterActivityAndFragmentDelegate {

	// host就是我们传递进来的FlutterActivity实例
  private Host host;
  
  FlutterActivityAndFragmentDelegate(@NonNull Host host) {
    this.host = host;
  }
}

我们继续看FlutterActivity的onCreate()方法,初始化delegate的时候传递的this(也就是delegate持有了当前FlutterActivity对象),初始化后依次调用了它的onAttach()和onActivityCreated()方法,最后调用了setContentView()方法为当前Activity添加视图,而在调用setContentView()的时候也是委托delegate的onCreateView()方法给当前当前Activity创建视图。我们先看下delegate的onAttach()方法,源码如下:

void onAttach(@NonNull Context context) {
  // 确保host进行过初始化
  ensureAlive();

  if (flutterEngine == null) {
    setupFlutterEngine();
  }

  platformPlugin = host.providePlatformPlugin(host.getActivity(), flutterEngine);

  if (host.shouldAttachEngineToActivity()) {
    Log.d(TAG, "Attaching FlutterEngine to the Activity that owns this Fragment.");
    flutterEngine.getActivityControlSurface().attachToActivity(
        host.getActivity(),
        host.getLifecycle()
    );
  }

  host.configureFlutterEngine(flutterEngine);
}

onAttach()方法首先调用ensureAlive()确保host实例化过,然后判断flutterEngine是否初始化过,在应用首次启动时flutterEngine为null,所以执行setupFlutterEngine()先初始化flutterEngine,setupFlutterEngine源码如下:

void setupFlutterEngine() {
  Log.d(TAG, "Setting up FlutterEngine.");

  // host是FlutterActivity实例,FlutterActivity的getCachedEngineId()方法返回null
  // First, check if the host wants to use a cached FlutterEngine.
  String cachedEngineId = host.getCachedEngineId();
  if (cachedEngineId != null) {
    flutterEngine = FlutterEngineCache.getInstance().get(cachedEngineId);
    isFlutterEngineFromHost = true;
    if (flutterEngine == null) {
      throw new IllegalStateException("The requested cached FlutterEngine did not exist in the FlutterEngineCache: '" + cachedEngineId + "'");
    }
    return;
  }

  // 调用FlutterActivity的provideFlutterEngine()方法,该方法默认返回null
  // Second, defer to subclasses for a custom FlutterEngine.
  flutterEngine = host.provideFlutterEngine(host.getContext());
  if (flutterEngine != null) {
    isFlutterEngineFromHost = true;
    return;
  }

  // Our host did not provide a custom FlutterEngine. Create a FlutterEngine to back our
  // FlutterView.
  Log.d(TAG, "No preferred FlutterEngine was provided. Creating a new FlutterEngine for"
      + " this FlutterFragment.");
  // 创建FlutterEngine实例
  flutterEngine = new FlutterEngine(host.getContext(), host.getFlutterShellArgs().toArray());
  isFlutterEngineFromHost = false;
}

setupFlutterEngine()方法的职责是初始化flutterEngine,首先根据host的getCachedEngineId()方法判断是否使用缓存的FlutterEngine,因为host是FlutterActivity实例而FlutterActivity的getCachedEngineId()方法通过Intent获取的,默认返回的是null,所以接着执行host的provideFlutterEngine()方法,而FlutterActivity的provideFlutterEngine()方法默认返回的也是null,所以最后直接调用了FlutterEngine的两个参数的构造方法创建一个FlutterEngine实例,FlutterEngine的构造方法如下:

public FlutterEngine(@NonNull Context context, @Nullable String[] dartVmArgs) {
  // 调用FlutterLoader的getInstance()方法返回FlutterLoader实例
  this(context, FlutterLoader.getInstance(), new FlutterJNI(), dartVmArgs, true);
}

public FlutterEngine(
    @NonNull Context context,
    @NonNull FlutterLoader flutterLoader,
    @NonNull FlutterJNI flutterJNI
) {
  this(context, flutterLoader, flutterJNI, null, true);
}

public FlutterEngine(
    @NonNull Context context,
    @NonNull FlutterLoader flutterLoader,
    @NonNull FlutterJNI flutterJNI,
    @Nullable String[] dartVmArgs,
    boolean automaticallyRegisterPlugins
) {
  this.flutterJNI = flutterJNI;
  // 再次调用FlutterLoader的startInitialization()方法开始初始化Flutter引擎,防止没有初始化的情况
  flutterLoader.startInitialization(context);
  // 调用FlutterLoader的ensureInitializationComplete()方法加载Flutter引擎需要的资源文件libapp.so
  flutterLoader.ensureInitializationComplete(context, dartVmArgs);

  flutterJNI.addEngineLifecycleListener(engineLifecycleListener);
  attachToJni();

  // 省略部分代码
}

FlutterEngine目前提供了三个构造方法,最后执行的是含有4个参数的构造方法,在该构造方法内部调用了FlutterLoader的startInitialization()和ensureInitializationComplete()方法,再次调用startInitialization()的目的是保证Flutter引擎已经加载过,那么调用ensureInitializationComplete()方法是做什么的了?我们看下它的源码,如下:

/**
 * Blocks until initialization of the native system has completed.
 * <p>
 * Calling this method multiple times has no effect.
 *
 * @param applicationContext The Android application context.
 * @param args Flags sent to the Flutter runtime.
 */
public void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) {
  	// 如果初始化过直接返回
    if (initialized) {
        return;
    }
  	// 必须在主线程调用
    if (Looper.myLooper() != Looper.getMainLooper()) {
      throw new IllegalStateException("ensureInitializationComplete must be called on the main thread");
    }
  	// 必须调用过startInitialization()方法
    if (settings == null) {
      throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization");
    }
    try {
      	// 只有在DEBUG或者JIT_RELEASE模式下resourceExtractor才非空
        if (resourceExtractor != null) {
            resourceExtractor.waitForCompletion();
        }

      	// shell参数
        List<String> shellArgs = new ArrayList<>();
        shellArgs.add("--icu-symbol-prefix=_binary_icudtl_dat");

        ApplicationInfo applicationInfo = getApplicationInfo(applicationContext);
        shellArgs.add("--icu-native-lib-path=" + applicationInfo.nativeLibraryDir + File.separator + DEFAULT_LIBRARY);

        if (args != null) {
            Collections.addAll(shellArgs, args);
        }

        String kernelPath = null;

        if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
            // 在DEBUG或者JIT_RELEASE模式下加载asset下的资源
            String snapshotAssetPath = PathUtils.getDataDirectory(applicationContext) + File.separator + flutterAssetsDir;
            kernelPath = snapshotAssetPath + File.separator + DEFAULT_KERNEL_BLOB;
            shellArgs.add("--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath);
            shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + vmSnapshotData);
            shellArgs.add("--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + isolateSnapshotData);
        } else {
          	// RELEASE模式下加载nativeLibraryDir下的libapp.so文件
            shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName);

          	// 这里很重要,如果libapp.so加载失败,可以设置libapp.so的全路径
            // Most devices can load the AOT shared library based on the library name
            // with no directory path.  Provide a fully qualified path to the library
            // as a workaround for devices where that fails.
            shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName);
        }

        shellArgs.add("--cache-dir-path=" + PathUtils.getCacheDirectory(applicationContext));
        if (settings.getLogTag() != null) {
            shellArgs.add("--log-tag=" + settings.getLogTag());
        }

        String appStoragePath = PathUtils.getFilesDir(applicationContext);
        String engineCachesPath = PathUtils.getCacheDirectory(applicationContext);
      	// 调用native方法加载libapp.so
        FlutterJNI.nativeInit(applicationContext, shellArgs.toArray(new String[0]),
            kernelPath, appStoragePath, engineCachesPath);

        initialized = true;
    } catch (Exception e) {
        Log.e(TAG, "Flutter initialization failed.", e);
        throw new RuntimeException(e);
    }
}

ensureInitializationComplete()方法和startInitialization()方法套路是一样的,它根据initialized字段允许我们多次调用,程序首次执行的时候initialized为false所以正常往下走,首先校验settings时候为null,因为settings是在startInitialization()方法中初始化的,如果为null就表示startInitialization()方法没有调用则直接抛异常,接着是判断resourceExtractor是否为null,resourceExtractor只有在DEBUG和JIT_RELEASE模式下才会初始化,所以此时resourceExtractor为null,然后定义了shellArgs集合,该集合装载的是Flutter引擎所需参数,具体参数可参考这里,继续看代码,在给shellArgs添加参数的时候有个判断,当编译模式为DEBUG和JIT_RELEASE的时候添加的参数和RELEASE模式下添加的参数是不同的,RELEASE模式下添加的是AOT_SHARED_LIBRARY_NAME参数并且该参数添加了两次,第一次添加的参数值是aotSharedLibraryName的值(aotSharedLibraryName在initConfig()进行的初始化,默认是libapp.so),第二次添加的值是applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName,也就是libapp.so的全路径,在仔细看第二次添加AOT_SHARED_LIBRARY_NAME参数前的一段注释,这个注释的信息非常重要:

if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
		// 省略代码
} else {
  	// 第一次添加
    shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName);

  	// 注意看这段注释,重要的事情说三遍
    // 注意看这段注释,重要的事情说三遍
    // 注意看这段注释,重要的事情说三遍
  	// 大部分的设备都可以直接加载libapp.so,如果加载失败就直接加载全路径的libapp.so
    // Most devices can load the AOT shared library based on the library name
    // with no directory path.  Provide a fully qualified path to the library
    // as a workaround for devices where that fails.
    shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName);
}

根据注释我们可以得到一个非常重要的结论:第二次添加AOT_SHARED_LIBRARY_NAME参数的目的是兜底操作,就是第一次直接加载libapp.so库失败的时候Flutter引擎会再次尝试加载全路径的libapp.so库,从而防止libapp.so加载失败。根据这个结论我们可以思考一下,如果让第一次加载失败,也即是让aotSharedLibraryName传一个错误的值,然后第二次传递一个我们指定的路径值,那么Flutter引擎加载的就是我们指定的libapp.so了,这也就可以实现Flutter的热更新了!有了这个思路我们接下来就是考虑如何修改这个全路径的值。

代码分析到这我们知道了libapp.so是在FlutterActivity的onCreate()方法中被加载的,具体加载是通过FlutterLoader的ensureInitializationComplete()方法实现的,在ensureInitializationComplete()方法中如果可以实现修改这个路径,那么Flutter的热修复功能就可以实现了。

修改这个全路径的值有多种方式,第一种是修改使用的flutter.jar包下的FlutterLoader.java的代码,给其添加可以设置路径的方法;另外一种是我们自己创建一个Loader类继承FlutterLoader,然后把FlutterLoader的代码拷贝过来并添加可以设置路径的方法,然后使用反射替换掉FlutterLoader的instance实例为我们自己创建的Loader实例,通过这种替换的方式当外界调用FlutterLoader的getInstance()方法时返回的就是我们自己的Loader实例,这两种方式都可以实热修复,这里我们将采用第二种方式(还有其他方式也可以实现热修复,等我后续有时间了再写出来吧)。

由于篇幅原因,我将在下篇文章Flutter源码系列之《一》Flutter的热更新探索(下) 实现热更新功能,敬请期待……

发布了39 篇原创文章 · 获赞 87 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/llew2011/article/details/104075796