《插件自助开发——客户端概述》

本文概括性介绍AnySDK 插件自助开发的一些容易混淆的定义,开发步骤和流程,配置,插件工程及工程记录等。在介绍过程中,简略说明相关的常用接口。

config.xml详细说明

在插件文件里,有一个必然存在的 config.xml文件,这是插件与客户端打包工具交互的配置文件。打包工具根据config.xml来显示SDK参数,配置插件内注册的插件类,判断插件更新等。
根节点是config,属性pluginId表示插件 ID,是插件唯一标志,与代码中保持一致。
子节点paramLs里面是第三方sdk需要用到的所有参数;子节点pluginLs是插件拥有的模块,比如用户模块、支付模块等等;子节点version是插件版本和更新内容等;子节点operateLs是预留操作字段。

安智插件中的config.xml全文如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <config pluginId="13">
      <paramLs>
         <param name="AnzhiAppKey" required="1" showName="AppKey" desc="应用的唯一标示,用于标示应用" bWriteIntoManifest="0" bUserOffer="1" bWriteIntoClient="1"/>
         <param name="AnzhiAppSecret" required="1" showName="AppSecret" desc="应用私钥" bWriteIntoManifest="0" bUserOffer="1" bWriteIntoClient="1"/>
         <param name="callback" required="1" desc="anysdk支付结果通知" showName="支付通知地址" bUserOffer="1" bWriteIntoClient="0"/>
      </paramLs>

      <operateLs>
      </operateLs>

      <pluginLs>
         <plugin name="UserAnZhi" typePlugin="0" desc="安智用户插件 "/>
         <plugin name="IAPOnlineAnZhi" typePlugin="2" desc="安智支付插件"/>
      </pluginLs>

      <version code = "1013" showVersion = "2.1.3_3.2" >
         <requestVersion minClientVer="2.0" minFrameworkVer="2.0"/>
         <![CDATA[更新框架2.0]]>
      </version>
    </config>     

子节点paramLs

子节点paramLs里面是第三方sdk需要用到的所有参数,这些参数配置显示在客户端打包工具的SDK参数列表中,上述安智插件参数显示如下图: SDK 参数显示

在插件代码中通过 PluginHelper 类中的getParamsInfo()接口获取用户输入的值。在这些参数中,有些是不能随便填写,只能选择一个可选值,每一个可选值是一个子标签option;有些不是填写字符串,而是选择文件的(如图片),需要添加一个子标签input。其配置分别如下:

     <param
        name="TCMSDKPayMode"
        bUserOffer="1"
        bWriteIntoClient="1"
        bWriteIntoManifest="0"
        desc="APP:使用应用传递的充值金额;SDK:由SDK提供充值默认值"
        required="1"
        showName="充值默认值" >

        <option value="APP" />

        <option value="SDK" />
    </param>

 

    <param
        name="TCMSDKCoinIcon"
        bUserOffer="1"
        bWriteIntoClient="1"
        bWriteIntoManifest="0"
        desc="支付时的道具图标,图标像素要求:48*48"
        placeholderText="图标像素要求:48*48"
        required="1"
        showName="道具图标" >

        <input
          data1="Image File(*.png)"
          data2="game_coin_icon.png"
          desc="支付时的道具图标,图标像素要求:48*48"
          height="48"
          type="file"
          width="48" />
    </param>

说明:

参数描述
子节点:
<option>标签:当该参数为下拉选项时,而非自己填写时,可在其param添加子节点option。value:option值,非中文字,在插件中获取到的参数值。
<input>标签:当需要用户传入文件时使用(如移动基地,需要用户传入一张图片作为展示图),拷贝至哪个目录则由python脚本的特殊需求来完成。

注意:
1. SDK必要参数必须提供参数配置,由用户填写。SDK可选参数中,常用设置项也必须提供参数由用户填写。
2. 必要参数在前,可选参数在后。
3. 游戏名、游戏版本号、调试模式、屏幕方向等通过框架PluginHelper类提供的函数来获取,不以参数的形式传入。

子节点pluginLs

插件拥有的模块,比如用户模块、支付模块等等。插件模块在此处的排列顺序决定了客户端打包插件时的顺序,也就是插件初始化时的顺序。要求先用户再支付。 以下为酷派插件中的配置,包括用户系统类名为UserCoolpad、支付系统类名为IAPOnlineCoolpad、统计系统类名为AnalyticsDataEye。

<pluginLs>
     <plugin
        name="UserCoolpad"
        desc="酷派用户插件"
        typePlugin="0" />

    <plugin
        name="IAPOnlineCoolpad"
        desc="酷派微支付"
        typePlugin="2" />

   <plugin 
       name = "AnalyticsDataEye" 
       typePlugin = "5" 
       desc = "DataEye"/>

</pluginLs>

说明:

子节点version

  <version
    code="1001"
    minClientVer="1.5"
    minFrameworkVer="1.5"
    showVersion="2.0.1_1.1.2" >
    <![CDATA[更新至1.1.2版本]]>
  </version>

说明:

子节点SDKLs

子节点SDKLs是同一个插件里,用来划分相对独立的模块的,这些模块功能互不影响,有各自独立的参数,可以认为是子 SDK。一般用在分享插件中的shareSDK和广告类插件中。
以有米广告为例,其参数管理界面和子 SDK 配置文件内容分别如下: 子 SDK 参数

<SDKLs>
    <SDK SDKName = "Banner" SDKType = "16" SDKShowName = "Banner">
        <param name = "youmiBannerPos" required = "1" desc = "Banner" showName = "Pos" bWriteIntoManifest = "0" bUserOffer = "1">
            <option value = "center"/>
            <option value = "top-middle"/>
            <option value = "top-left"/>
            <option value = "top-right"/>
            <option value = "bottom-middle"/>
            <option value = "bottom-left"/>
            <option value = "bottom-right"/>
        </param>
    </SDK>
    <SDK SDKName = "FullScreen" SDKType = "16" SDKShowName = "插屏广告">
    </SDK>
    <SDK SDKName = "OfferWall" SDKType = "16" SDKShowName = "积分墙">
    </SDK>
</SDKLs>

说明:SDKLs中一个SDK子节点表示一个子 SDK,其中:

注意:所有参数不可重名,即param的name值不可以相同。

子节点operateLs

该节点为预留操作节点,大部分特殊操作已经移动到特殊脚本的OpenApi中

子节点sysFrameworksLs

iOS特有节点,配置 SDK 要求引入的系统库。
Xcode7后系统动态库后缀从.dylib改为.tbd,2.1.0版本以后已做兼容

 <sysFrameworksLs>
    <sysFrameworks name = "AudioToolbox.framework" path = "xcodeFrameworks" required = "1"/>
       <sysFrameworks name = "AdSupport.framework" path = "xcodeFrameworks" required = "1"/>
       <sysFrameworks name = "CoreTelephony.framework" path = "xcodeFrameworks" required = "1"/>
       <sysFrameworks name = "CoreGraphics.framework" path = "xcodeFrameworks"  required = "1"/>
       <sysFrameworks name = "CoreText.framework" path = "xcodeFrameworks" required = "1"/>
       <sysFrameworks name = "MessageUI.framework" path = "xcodeFrameworks" required = "1"/>
       <sysFrameworks name = "SystemConfiguration.framework" path = "xcodeFrameworks" required = "1"/>
       <sysFrameworks name = "Security.framework" path = "xcodeFrameworks" required = "1"/>
       <sysFrameworks name = "QuartzCore.framework" path = "xcodeFrameworks" required = "1"/>
       <sysFrameworks name = "UIKit.framework" path = "xcodeFrameworks" required = "1"/>
       <sysFrameworks name = "Foundation.framework" path = "xcodeFrameworks" required = "1"/>
       <sysFrameworks name = "Accelerate.framework" path = "xcodeFrameworks" required = "1"/>
       <sysFrameworks name = "AddressBook.framework" path = "xcodeFrameworks" required = "1"/>
       <sysFrameworks name = "AddressBookUI.framework" path = "xcodeFrameworks"   required = "1"/>
       <sysFrameworks name = "MobileCoreServices.framework" path = "xcodeFrameworks" required = "1"/>
       <sysFrameworks name = "CFNetwork.framework" path = "xcodeFrameworks" required = "1"/>
       <sysFrameworks name = "libsqlite3.0.dylib" path = "xcodeUsrlib" required = "1"/>
       <sysFrameworks name = "libstdc++.6.dylib" path = "xcodeUsrlib" required = "1"/>
       <sysFrameworks name = "libxml2.2.dylib" path = "xcodeUsrlib" required = "1"/>
    <sysFrameworks name = "libz.dylib" path = "xcodeUsrlib" required = "1"/>
</sysFrameworksLs>

说明:

script.py特殊脚本与OpenApi详细说明

目前打包过程中的绝大多数渠道特殊处理脚本已经被抽象到OpenApi类中,插件也在逐步更新
旧写法脚本为script.pyc, 新写法脚本为script.py,如果有需要,可以在新写法脚本中加入自己的特殊操作方法。因为更新插件时会覆盖原脚本,所以如果修改请做好备份

Android

初始化:先实例化一个api_obj, 再使用api_obj.方法名调用方法。

from any_open_api import AnyOpenAPI    
def script(SDK, decompile_dir, package_name, usrSDKConfig):
api_obj = AnyOpenAPI(SDK, decompile_dir, package_name, usrSDKConfig)

get_param_value(param): 获取参数值方法

@调用例子: api_obj.get_param_value("BDChannelId")    
@param param_name config.xml中对应的参数名    
@return 返回对应参数值,若是参数未找到,返回空字符串

get_file_absolute_path(file_name): 获取文件的绝对路径方法

@调用例子: api_obj.get_param_value("res/values/strings.xml")      
@param file_name:APK中的相对文件路径      
@return 返回在打包临时文件夹中该文件的绝对路径地址,若是参数未找到,返回空字符串

get_start_activity(): 获取启动的Activity

@调用例子: api_obj.get_start_activity()    
@return 返回找到的启动Activity名,若是失败返回None

modify_start_activity(new_activity_name): 修改Apk的启动Activity

@调用例子: api_obj.modify_start_activity("com.anysdk.sample.MainActivity")       
@param new_activity_name:新的启动Activity名称,如:com.anysdk.sample.MainActivity       
@return:成功则返回原先的启动Activity以供其他位置修改,失败返回None

modify_application_or_inherit(application_name): 主要用于解决以下两种情况:
1.在母包未指定Application的时候,指定Application名称
2.在已有Application的时候,让其Application继承指定的Application

@调用例子: api_obj.modify_application_or_inherit("com.anysdk.framework.DemoApplication")      
@param application_name:指定的Application    
@return:返回修改结果是否成功(True/False)

modify_strings_values(file_name, key_name, value, is_add_when_not_existed=False): 修改apk的中xml字符串资源

@调用例子: api_obj.modify_strings_values("res/values/g_strings.xml", "g_class_name", origin_activity)     
@param file_name 指定的文件,如res/values/strings.xml    
@param key_name 要修改的string节点的name值      
@param value 要修改的string值        
@param is_add_when_not_existed: 如果该字段不存在是否添加进去,默认不添加       
@return 返回操作结果,成功与否(True/False)

set_application_attr(attr_key, attr_value): 修改AndroidManifest.xml中的application属性

@调用例子: api_obj.set_application_attr("key", "value")    
@param attr_key 属性名    
@param attr_value 属性值     
@return 返回操作结果,成功与否(True/False)

get_application_attr(self, attr_key): 获取AndroidManifest.xml中的application属性

@调用例子:api_obj.get_application_attr("key")    
@param attr_key 属性名    
@return 成功返回获取结果,若失败返回None

add_meta_data(key_name, key_value_param_name, front, back):添加Meta-data键值。一些需要脚本计算后才能填写的meta-data可用此方法

@调用例子:api_obj.add_meta_data("ANQUSDK_PRIVATEKEY", anqu_value, "", "")     
@param key_name meta-data的android:name值      
@param key_value_param_name meta-data的android:value值(传入配置的参数名,由打包工具去解析获取真实值,若无此参数则取填写的字段)    
@param front meta-data的android:value值的前缀     
@param back meta-data的android:value值的后缀,最终的value值为 front+[param值]+back     
@return 返回操作结果,成功与否(True/False)

write_information_into_file(file_name, information):向指定文件覆盖写入信息

@调用例子:api_obj.write_information_into_file("/assets/ztsdk_config.properties", "CONFIG_APPID = 1")     
@param file_name 相对文件路径    
@param information 要写入的信息    
@return 返回操作结果,成功与否(True/False)

copy_rfiles(target_dir):拷贝R文件,部分SDK会默认读取指定目录下的R文件,此函数用于根据当前资源生成R文件并拷贝至指定目录

@调用例子:api_obj.copy_rfiles("mobi/zty/pay")    
@param target_dir:拷贝指定的目录     
@return:返回该操作是否成功(True/False)

copy_smali_file(file_name, origin_path, target_path):拷贝代码文件

@调用例子:api_obj.copy_smali_file("WXPayEntryActivity", packageName + ".wxapi", "com/anysdk/framework/wxapi")      
@param file_name 要拷贝的代码文件名,不包含后缀     
@param origin_path 代码原所在位置      
@target_path 目标位置       
@return 返回操作结果,成功与否(True/False)

copy_smali_to_package_name(file_name, origin_path, target_path):拷贝代码文件到包名目录下

@调用例子:api_obj.copy_smali_file("WXPayEntryActivity", packageName + ".wxapi", "framework/wxapi")     
@param file_name 要拷贝的代码文件名,不包含后缀     
@param origin_path 代码原所在位置      
@param target_path 包名目录下的指定路径      
@return 返回操作结果,成功与否(True/False)

copy_file(param_name, target_path):拷贝文件到指定目录下

@调用例子:api_obj.copy_file("BPGameIcon", "res/drawable-xhdpi/bp_gameicon_64.png")     
@param param_name:源资源所在目录(传入参数名,由打包工具去解析获取真实路径,若是相对路径会去查找config/game/gameid/channel目录)      
@param target_path:拷贝到的指定位置(如Res/Drawable/game_icon.png)      
@return 返回操作结果,成功与否(True/False)

iOS

初始化:先实例化一个api_obj, 再使用api_obj.方法名调用方法。

from any_open_api import AnyOpenAPI     
def script(SDK, work_dir, target_name, usrSDKConfig, SDKDestDir, project):     
     api_obj = AnyiOSOpenAPI(SDK, work_dir, target_name, usrSDKConfig,      SDKDestDir, project)

get_param_value(param): 获取参数值方法

@调用例子: api_obj.get_param_value("BDChannelId")    
@param param_name config.xml中对应的参数名     
@return 返回对应参数值,若是参数未找到,返回空字符串

get_file_absolute_path(file_name):获取文件的绝对路径

@调用例子:api_obj.get_file_absolute_path("AppController.mm")     
@param file_name 文件名,如"AppController.mm",必须为已经加入工程的文件名     
@return 返回绝对路径,如果工程中获取不到,则返回空值

get_bundle_id():获取BundleID

@return:返回BundleID值

modify_archs(archs):修改Architectures指令集, 取原工程和所选指令集的交集

@调用例子:api_obj.modify_archs("armv7 arm64")    
@param archs:修改后的指令集,以空格分隔    
@return: 返回操作结果,修改与否(True/False),若archs都已经存在则返回False

add_ldflags(flags):添加OtherLinkerFlags参数

@调用例子:api_obj.add_ldflags("-ld  -all_load")     
@param flags 要添加的OtherLinkerFlags,以空格分隔     
@return: 返回操作结果,修改与否(True/False),若flags都已经存在则返回False

add_plist_url_schemes(schemes, name="", front="", back=""):添加UrlSchemes

@调用例子:api_obj.add_plist_url_schemes("Bundle_ID", "weixin", "wx", "")    
@param schemes 要添加的schemes的参数名/值,若写为"Bundle_ID",则取bundle id值,若有对应的参数名则取参数值,否则取文本    
@param name 若需要填写Identifier填写在此    
@param front 填写在scheme前的文本     
@param back:填写在scheme后的文本,最终的schemes为front+schemes取值+back

Android 插件工程说明

工程目录

插件工程目录结构基于Eclipce创建的 Android 项目进行,建议使用 Eclipse 进行开发。
工程目录比较
新增目录有:

插件工程以相对路径的方式引入了插件开发过程中用到的框架jar 包——libPluginProtocol.jar,需要注意的是,这个 jar 包仅在编译时用到,不会编入最终发布的插件中。

ForManifest

第三方SDK会有要求在AndroidManifest.xml中添加sdk的activity、service、 receiver、权限等。我们把这些内容放入ForManifest.xml,供打包工具读取后添加到渠道包中,实现第三方SDK的要求。
ForManifest文件夹通常只有ForManifest.xml,如果第三方SDK需要分别对横竖屏进行添加时可分为ForManifestLandscape.xml和ForManifestPortrait.xml分别编写(也就是说 ForManifest 文件夹下有ForManifest.xml或者ForManifestLandscape.xml和ForManifestPortrait.xml)。客户端打包工具会根据的渠道参数中设置的横竖屏来判断使用ForManifestLandscape.xml还是ForManifestPortrait.xml。

我们以内容比较少的金立插件为例来说明,其ForManifest.xml全部内容如下:

<?xml version="1.0" encoding="utf-8"?>
<manifestConfig xmlns:android="http://schemas.android.com/apk/res/android" >

   <permissionCfg>

      <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

      <uses-permission android:name="android.permission.GET_TASKS" />

      <uses-permission android:name="android.permission.INTERNET" />

      <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

      <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

      <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />

      <uses-permission android:name="android.permission.READ_PHONE_STATE" />

      <uses-permission android:name="android.permission.INSTALL_PACKAGES" />

      <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

      <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
   </permissionCfg>

   <applicationCfg keyword="com.gionee.gsp.floatingwindow.FloatingWindowService" >

     <!-- Amigo Play SDK组件声明开始 -->

      <service android:name="com.gionee.gsp.floatingwindow.FloatingWindowService" >

         <intent-filter>

             <action android:name="com.gionee.pay.ic.FloatingWindowService" />

             <category android:name="android.intent.category.DEFAULT" />
         </intent-filter>
      </service>

      <activity
         android:name="com.gionee.gsp.service.activity.AssistActivity"
         android:configChanges="mcc|mnc|orientation|screenSize"
         android:theme="@android:style/Theme.Translucent.NoTitleBar" />
      <!-- Amigo Play SDK组件声明结束 -->

   </applicationCfg>

 </manifestConfig>

说明: ForManifest.xml文档的根节点是manifestConfig,子节点:

编码注意事项

1.线程问题 在调用SDK 提供的可能涉及到页面的接口时,如登录、登出、支付、显示/隐藏悬浮窗、显示广告等,必须在主线程中调用,否则会导致崩溃或者界面不正常显示等错误。

通过调用框架提供的函数 PluginWrapper.runOnMainThread实现在主线程调用,百度游戏登出如下:

public void logout() {
    PluginWrapper.runOnMainThread(new Runnable() {
        @Override
        public void run() {
            BDGameSDK.logout();     //登出
            BDYouxiWrapper.setLogined(false); //设置登录状态
            actionResult(UserWrapper.ACTION_RET_LOGOUT_SUCCESS, "logout success"); //回调
            }
        });
    }

2.主 Activity 的回调 SDK的很多接口要求在主 Activity 的回调中调用,如销毁等,在插件里通过调用框架提供的PluginWrapper.setActivityCallback函数将监听添加到框架的队列里,框架在主Activity回调时,调用相应的接口。一般情况下,一个插件中,只设置一次。

以百度游戏为例,其代码如下:

PluginWrapper.setActivityCallback(new IActivityCallback() {

        @Override
        public void onStop() {
            LogD("BDYouxiWrapper", "onStop");
            mActivityAdPage.onStop();               
        }

        @Override
        public void onResume() {
            LogD("BDYouxiWrapper", "onResume");
            mActivityAnalytics.onResume();
            mActivityAdPage.onResume();
        }

        @Override
        public void onRestart() {

        }

        @Override
        public void onPause() {
            mActivityAnalytics.onPause();
        }

        @Override
        public void onNewIntent(Intent intent) {

        }

        @Override
        public void onDestroy() {
            BDGameSDK.destroy();
        }

        @Override
        public void onActivityResult(int requestCode, int resultCode, Intent data) {

        }
    });

详细开发文档

用户系统开发文档
支付系统开发文档
推送系统开发文档
分享系统开发文档
统计系统开发文档
广告系统开发文档

一个插件中可以包含多个系统,如渠道类的一般都有用户系统和支付系统,酷派有用户系统、支付系统和统计系统,360有用户系统、支付系统、统计系统和推送系统。
当一个插件中有多个系统存在时,就会有些数据或方法是每个系统都需要用到的,如 SDK 版本号,插件 ID,主 Activity 回调,登录及登录验证等,我们把这些公共的数据和方法抽象到一个类中,命名为"插件名+Wrapper"。详细示例请参考已开源代码。

iOS 插件工程说明

iOS 插件工程是一个拥有特殊目录结构的静态库工程。

工程目录

自助开发工具生成的 iOS插件工程的目录结构如下(以模板为例,即插件名为 iOS_Template):
iOS 插件工程目录

说明:

项目结构

可读性和可维护性,我们对项目结构制定了规则,我们以快用为例,其截图如下:
iOS 插件工程结构
说明:

详细开发文档

用户系统开发文档
支付系统开发文档
广告系统开发文档
统计系统开发文档
推送系统开发文档
分享系统开发文档

一个插件中可以包含多个系统,如渠道类的一般都有用户系统和支付系统。有的渠道包含更多的系统。当一个插件中有多个系统存在时,就会有些数据或方法是每个系统都需要用到的,如 SDK 版本号,插件 ID,登录及登录验证等,我们把这些公共的数据和方法抽象到一个类中,命名为"插件名+Wrapper"。

README.md

每一个插件都对应一份README.md文件,记录插件的更新历史。
在自助开发工具中配置后,可自动生成,但开始开发时更新内容和注意实现可能不完整,所以记得开发完成后补充完整。
本文件的查看者有:
1.测试人员:根据更新内容和注意实现进行测试
2.开发人员:根据文件内容进展后续开发和维护
3.技术支持:根据注意实现补充SDK 注意事项等,提醒用户。
注意:插件更新时,在顶部增加内容,只允许增加内容,不允许删除已有内容;采用Markdown格式书写。
文件显示格式如下:

XXXX年XX月XX日 
客户端开发:email 
服务端开发:email 
插件版本:x.x.x_x.x.x.x
更新内容:
 1.更新内容1 
 2.更新内容2
注意事项:
 1.注意事项1
 2.注意事项2

说明:

腾讯MSDK的注意事项比较多, 我们以它为例,其内容(Markdown格式书写)如下:

###2015年3月23日 
客户端开发:daincameng@anysdk.com  
服务端开发:xqp  
插件版本:2.0.0_2.5.3a   
更新内容:  
1.更新至2.5.3a版本  
注意事项:  
1.必须在onCreate()中加载插件,否则可能导致登录回调丢失  
2.如果游戏的Activity为Launch Activity, 则需要在游戏Activity声明中添加android:configChanges="orientation|screenSize|keyboardHidden", 否则可能造成没有登录没有回调。  
3.微信授权需要保证微信版本高于4.0  
4.拉起微信时候, 微信会检查应用程序的签名和微信后台配置的签名是否匹配(此签名在申请微信appId时提交过), 如果不匹配则无法唤起已经授权过的微信客户端.   
5.游戏点击注销按钮或者其余弹出登录框的逻辑中都必须要调用Logout来清空本地的登录信息。否则会导致授权失败等问题  
6.微信accessToken只有两个小时的有效期,不采用自动刷新模式,但用到accessToken时发现失效,自动完成重新登录流程,并回调登录消息  
7.微信refreshToken的有效期为30天,过期后回调登录失败消息,需要重新登录,一般不会出现这种情况。
8.jar包编译时注意,需要区分大小写。  
9.特别注意:测试模式下无法显示支付界面,必须收到安装支付的测试插件才可用。  
10.微信帐号和QQ帐号在支付时,服务端请求参数都要用手Q的APPID和APPKEY,否则会提示没有支付权限。

在10条注意事项中:
1、2是登录没有回调的可能原因;
3、4、5是微信授权失败的可能原因;
6是说明存在这样的情况:未调用微信登录授权却出现登录授权界面,原因是accessToken无效了。
7是自动登录失败的可能原因,概率比较小;
8是插件打包成 zip 的编译时,必需在区分大小写的磁盘上进行;
9是调试模式的特殊处理;
10是维护插件时的注意点。