android studio 自定義模板

由于項(xiàng)目用上了 mvp 架構(gòu),基本上一個(gè)頁(yè)面就至少需要新創(chuàng)建6個(gè)類(lèi),分別是 model view presenter 的接口以及其對(duì)應(yīng)的實(shí)現(xiàn)類(lèi),再加上使用 dagger 的話就要更多了,所以這時(shí)候 android studio 的自定義模板就派上用場(chǎng)了,可以節(jié)省很多編寫(xiě)模板代碼的重復(fù)性工作

那么該如何入手呢?相信大部分用過(guò) as 的人以及使用過(guò)一些自帶的模板樣式了,這些自帶的模板就是最好的參照目標(biāo)了,廢話不多說(shuō),先看看它的結(jié)構(gòu)

1.模板結(jié)構(gòu)

這里參照的是 empty activity

Empty Activity

它的位置就在 as的安裝目錄(mac的話右鍵as應(yīng)用-> 顯示包內(nèi)容 -> content 里就是了)/plugins/android/lib/templates/activities,

模板的結(jié)構(gòu)

這里簡(jiǎn)單做個(gè)總結(jié):

  • template:主要是給生成頁(yè)面提供一些需要用戶傳入的參數(shù)
  • global.xml.ftl:主要提供一些全局參數(shù)
  • recipe.xml.ftl:主要用于生成實(shí)際需要的代碼,資源文件等
  • root文件夾:包含 project 中一系列屬性文件的模板

root 底下還有一些相關(guān)文件介紹

  • build.gradle.ftl:project 的 build.gradle 模板,如果需要添加 maven 庫(kù)的地址,就在這里添加
  • gradle.properties.ftl:project 的 gradle.properties 的模板,如果需要添加工程的一些公用屬性(版本號(hào)\版本名\簽名信息\私有 maven 庫(kù)的 group 和 id 信息等)就在這里面修改
  • local.properties.ftl:project 的 local.properties.ftl 模板,里面指定 SDK的路徑,如果設(shè)置好環(huán)境變量,創(chuàng)建工程的時(shí)候就動(dòng)態(tài)生成指定的路徑,不需要手動(dòng)修改
  • project_ignore:project 的.gitingore 模板,里面可以增刪版本管理需要過(guò)濾的文件夾\文件
  • settings.gradle.ftl:project 的 settings.gradle 模板,里面可以指定真?zhèn)€工程需要編譯的 module,這個(gè)建議不要修改,可以在工程中手動(dòng)修改

1.1首先是 template.xml 文件,打開(kāi)后的主要內(nèi)容如下

<?xml version="1.0"?>
<template
    format="5"
    revision="5"
    name="Empty Activity"
    minApi="9"
    minBuildApi="14"
    description="Creates a new empty activity">

<category value="Activity" />
<formfactor value="Mobile" />

<parameter
    id="activityClass"
    name="Activity Name"
    type="string"
    constraints="class|unique|nonempty"
    suggest="${layoutToActivity(layoutName)}"
    default="MainActivity"
    help="The name of the activity class to create" />
<!-- 省略了若干個(gè) parameter,和上面那個(gè)差不多 -->
<!-- 128x128 thumbnails relative to template.xml -->
<thumbs>
    <!-- default thumbnail is required -->
    <thumb>template_blank_activity.png</thumb>
</thumbs>

<globals file="globals.xml.ftl" />
<execute file="recipe.xml.ftl" />

</template>

其中
1.<template>的 name 屬性,對(duì)應(yīng)新建 Activity 時(shí)顯示的名字
2.<category>對(duì)應(yīng) New 的類(lèi)別為 Activity

頁(yè)面和屬于對(duì)照

現(xiàn)在來(lái)詳解 parameter標(biāo)簽 的屬性

  • id:唯一表示,最終通過(guò)這個(gè)屬性來(lái)獲取輸入值(分為input 和 checkbox)
  • name:相當(dāng)于 hint 了
  • type:屬性的類(lèi)型,分為 String 和 Boolean
  • constraints:填寫(xiě)值的約束
  • suggest:建議值
  • default:默認(rèn)值
  • visibility:是否顯示(一般就是根據(jù)其他類(lèi)型為 checkbox 的 parameter 來(lái)確定了),例如上圖的 layoutname,只有 generateLayout 為 true 時(shí)才顯示
generateLayout 為 false 時(shí)不顯示 Layout Name
generateLayout 為 true 時(shí)顯示 Layout Name
  • help:鼠標(biāo)懸浮在該 parameter 時(shí)顯示的幫助提示


    help 屬性的效果

然后是 thumbs 標(biāo)簽,也沒(méi)啥,就是個(gè)縮略圖罷了

thumbs

最后還有兩個(gè)標(biāo)簽,引用了外部文件,也是下面要講的內(nèi)容
<globals file="globals.xml.ftl" />
<execute file="recipe.xml.ftl" />

1.2 globals.xml.ftl

  <globals>
    <global id="hasNoActionBar" type="boolean" value="false" />
    <global id="parentActivityClass" value="" />
    <global id="simpleLayoutName" value="${layoutName}" />
    <global id="excludeMenu" type="boolean" value="true" />
    <global id="generateActivityTitle" type="boolean" value="false" />
    <#include "../common/common_globals.xml.ftl" />
  </globals>

里面定義的是一些全局變量,方便其他文件可以引用這里的值,引用的方式是&{id的值}

最后可以看到還引用了另外一個(gè) ftl,這也說(shuō)明了這個(gè)文件里定義的屬性同時(shí)也可以被其他模板引用

1.3 recipe.xml.ftl

   <?xml version="1.0"?>
   <recipe>
    <#include "../common/recipe_manifest.xml.ftl" />

    <#if generateLayout>
      <#include "../common/recipe_simple.xml.ftl" />
      <open file="${escapeXmlAttribute(resOut)}/layout/${layoutName}.xml" />
    </#if>

    <instantiate from="root/src/app_package/SimpleActivity.java.ftl"
               to="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />

    <open file="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
  </recipe>

跳過(guò)兩個(gè) include 引入的 ftl,先介紹能看到的標(biāo)簽

  • open:在代碼生成后,打開(kāi)指定的文件,這里寫(xiě)了兩個(gè) open,所以創(chuàng)建了一個(gè) activity 后,就會(huì)把 activity 的 java 文件和layout.xml 同時(shí)打開(kāi)
  • instantiate:就是把模板轉(zhuǎn)換成實(shí)際目標(biāo)文件的一個(gè)操作了,from 指定的是模板文件,to 指定的是生成文件,后面再詳細(xì)介紹

然后可以看到前面還 include 了兩個(gè) ftl,實(shí)際上代表的就是 menifest 和 layout 的相關(guān)操作,下面是 recipe_manifest.xml.ftl 的內(nèi)容

<recipe folder="root://activities/common">
    <merge from="root/AndroidManifest.xml.ftl"
         to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />
    <merge from="root/res/values/manifest_strings.xml.ftl"
         to="${escapeXmlAttribute(resOut)}/values/strings.xml" />
</recipe>

這里又看到一個(gè)新的標(biāo)簽merge,字面意義就是合并,也就是把模板文件合并到項(xiàng)目中已經(jīng)存在的對(duì)應(yīng)文件中,這里是合并了 AndroidManifest.xml 和 string.xml

recipe中還有一個(gè)比較常見(jiàn)的標(biāo)簽,這個(gè)模板里沒(méi)看到
<copy from="root/res/drawable-hdpi" to="${escapeXmlAttribute(resOut)}/drawable-hdpi" />

  • copy :從root中copy文件到我們的目標(biāo)目錄,比如我們的模板Activity需要使用一些圖標(biāo),那么可能就需要使用copy標(biāo)簽將這些圖標(biāo)拷貝到我們的項(xiàng)目對(duì)應(yīng)文件夾。

2.代碼生成的過(guò)程

模板里的文件基本都是 ftl 結(jié)尾的, 這里首先需要要解釋一下 ftl 的概念

ftl是FreeMarker Template Language的縮寫(xiě),它是簡(jiǎn)單的,專用的語(yǔ)言, 不是 像PHP那樣成熟的編程語(yǔ)言。 那就意味著要準(zhǔn)備數(shù)據(jù)在真實(shí)編程語(yǔ)言中來(lái)顯示,比如數(shù)據(jù)庫(kù)查詢和業(yè)務(wù)運(yùn)算, 之后模板顯示已經(jīng)準(zhǔn)備好的數(shù)據(jù)。在模板中,你可以專注于如何展現(xiàn)數(shù)據(jù), 而在模板之外可以專注于要展示什么數(shù)據(jù)。

而AS中的這些模板是就是通過(guò)這個(gè)FreeMarker模板引擎創(chuàng)建的

FreeMarker 是一款 模板引擎: 即一種基于模板和要改變的數(shù)據(jù), 并用來(lái)生成輸出文本(HTML網(wǎng)頁(yè),電子郵件,配置文件,源代碼等)的通用工具。 它不是面向最終用戶的,而是一個(gè)Java類(lèi)庫(kù),是一款程序員可以嵌入他們所開(kāi)發(fā)產(chǎn)品的組件。

代碼生成的簡(jiǎn)單示意圖

3.簡(jiǎn)單的 freemarker 語(yǔ)法

  • 1.插入屬性

    定義好一個(gè)屬性,在模板文件中使用${定義好的屬性名稱},即可完成替換

  • 2.if 語(yǔ)法
    例如前面在 recipe.xml.ftl 里看到的, 這個(gè)generateLayout 是再 template 中定義的 boolean 的 parameter
    
    <#if generateLayout>
        <#include "../common/recipe_simple.xml.ftl" />
        <open file="${escapeXmlAttribute(resOut)}/layout/${layoutName}.xml" />
    </#if>
    

下面以 Empty Activity模板中的 SimpleActivity.java.ftl 為例子

package ${packageName}; import ${superClassFqcn}; import android.os.Bundle; <#if includeCppSupport!false> import android.widget.TextView; </#if> public class ${activityClass} extends ${superClass} { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); <#if generateLayout> setContentView(R.layout.${layoutName}); </#if> <#include "../../../../common/jni_code_usage.java.ftl"> } <#include "../../../../common/jni_code_snippet.java.ftl"> }

可以看到模板中有很多變量需要替換后才能生成為最終需要的代碼,這些變量一般來(lái)自 globals.xml.ftl 中預(yù)先定義好的變量以及 template 中需要用戶輸入的變量,經(jīng)過(guò) recipe.xml.ftl 中的instantiate標(biāo)簽指定生成的路徑即可完成這個(gè)過(guò)程

4.具體示例

最近的項(xiàng)目用到 mvp 和 dagger(這里就不細(xì)談 dagger 的用法了),所以每個(gè)頁(yè)面要多寫(xiě)很多接口以及實(shí)現(xiàn)類(lèi),下面是項(xiàng)目的分包:

包結(jié)構(gòu)
  • contract:定義了一個(gè)頁(yè)面的 presenter 和 view 的接口,放在contract 里是為了方便查看
  • di.component.presenter:用于往目標(biāo)類(lèi)注入 presenter
  • di.module:提供presenter所依賴的組件
  • model.event:model的接口
  • model.impl:model 的實(shí)現(xiàn)類(lèi)
  • presenter:contract 的 presenter 實(shí)現(xiàn)類(lèi)

4.1 模板代碼分析

下面分析一下,編寫(xiě)一個(gè)具體業(yè)務(wù)要生成哪些模板代碼,例如要做一個(gè)登錄的業(yè)務(wù)

4.1.1 mvp 的部分
  • 首先要定義的是Contract, 包含整個(gè)業(yè)務(wù)邏輯與頁(yè)面顯示
    public interface LoginContract {
    // Contract 中肯定是要包括 View 和 Presenter 的
    interface View {
    // 具體的方法,這部分不是模板能解決的
    void showLoginSuccess(UserInfo info);
    void showLoginFailed(Throwable e);
    }
    interface Presenter {
    // 具體的方法,這部分不是模板能解決的
    void loginIn(String account, String password);
    }
    }

  • ** View 的實(shí)現(xiàn),一般就是讓 activity 或者 fragment 實(shí)現(xiàn) LoginContract.View 了,這部分不是模板能解決的,就不寫(xiě)了**

  • 然后是 Presenter 的實(shí)現(xiàn),當(dāng)然實(shí)現(xiàn) LoginContract.Presenter 即可
    //項(xiàng)目有Presenter 的基類(lèi)的話模板里還需要添加繼承
    public class LoginPresenter extends BasePresenter implement LoginContract.Presenter {
    // 接口定義的方法,也不是模板能解決的
    public void loginIn(String account, String password) {
    // 具體的實(shí)現(xiàn)代碼
    }
    }

4.1.2 Presenter的部分

這里presenter 的具體實(shí)現(xiàn)實(shí)際上也是包含很多重復(fù)代碼的

  • 首先,Presenter 里肯定需要持有 上面定義的Contract.View 的引用,這樣才能再邏輯處理結(jié)束后回調(diào) View 層代碼

  • 然后,Presenter 也需要持有 Model層的引用去處理數(shù)據(jù),一般 Model 層也是需要定義接口的,所以又多了兩個(gè)類(lèi):LoginModel 和 LoginModelImpl

    public interface LoginModel {
      void login(String account, String password);
    }
    
    public class LoginModel extends BaseModel implement LoginModel {
      public void login(String account, String password) {
          // 具體代碼實(shí)現(xiàn)
      }
    }
    

修改后的 Presenter 代碼為

public class LoginPresenter extends BasePresenter implement LoginContract.Presenter {
  private LoginContract.View mView;
  private LoginModel mModel;
  
  public LoginPresenter(LoginContract.View view, LoginModel model) {
    mView = view;
    mModel = model;
  }
  
  // 接口定義的方法,也不是模板能解決的 
  public void loginIn(String account, String password) { // 具體的實(shí)現(xiàn)代碼 }
}

由于一般的業(yè)務(wù)都是要通過(guò)請(qǐng)求或者本地?cái)?shù)據(jù)庫(kù)來(lái)處理的,所以這里抽取父類(lèi) BaseModel,項(xiàng)目里使用了 GreenDao 和 Retrofit,所以 BaseModel依賴于DaoMaster.DevOpenHelper和 Retrofit 兩個(gè)對(duì)象

public class BaseModel {
  protected final Retrofit retrofit;
  protected final DaoMaster.DevOpenHelper dbOpenHelper;

  public BaseModel(DaoMaster.DevOpenHelper helper, Retrofit retrofit) {
    this.dbOpenHelper = helper;
    this.retrofit = retrofit;
  }
}

修改后的 LoginModelImpl的代碼為

public class LoginModelImpl extends BaseModel implement LoginModel {
    public LoginModelImpl(DaoMaster.DevOpenHelper helper, Retrofit retrofit) {
      super(helper, retrofit);
    }

    public void login(String account, String password) {
       // 具體代碼實(shí)現(xiàn) 
    }
}

到這里登錄業(yè)務(wù)的 P層和 M 層代碼基本就寫(xiě)完了,一共需要 LoginContract/ LoginPresenter/LoginModel/LoginModelImpl四個(gè)文件

4.1.3 dagger 的部分

首先這里說(shuō)一下 dagger 的好處,簡(jiǎn)單來(lái)說(shuō),dagger 就是將目標(biāo)類(lèi)與其依賴的對(duì)象的實(shí)例化過(guò)程隔離開(kāi)來(lái),例如這里的 LoginPresenter,一般在 activity 或者 fragment 中實(shí)例化

public class LoginActivity extends Activity implement LoginContract.View {
  private LoginPresenter mPresenter;

  public void onCreate(Bundle saveInstanceState) {
    super.onCrate(saveInstanceState);
    // 省略DaoMaster.DevOpenHelper 和 Retrofit 的實(shí)例化
    ....
    mPresenter = new LoginPresenter(this, new LoginModelImpl(helper, retrofit));
  }

  void showLoginSuccess(UserInfo info){...} 
  void showLoginFailed(Throwable e){...}
}

實(shí)際上寫(xiě)這種 new 的代碼是很 low 的,萬(wàn)一 LoginPresenter 的構(gòu)造函數(shù)被修改了,就需要修改 LoginActivity 的代碼,如果這個(gè) LoginPresenter 到處都是的話,那就悲催了...

所以dagger就是為了解決這個(gè)問(wèn)題而存在的,dagger 是一種依賴注入, 此處 LoginActivity 依賴于 LoginPresenter, dagger 可以把 LoginPresenter 的實(shí)例化放在一個(gè)獨(dú)立的模塊中去執(zhí)行,而 LoginActivity 不必關(guān)心也不知曉 Presnter 的實(shí)例化過(guò)程,這樣上面的問(wèn)題就迎刃而解了.至于 dagger 的用法這里就忽略了

接下來(lái)講使用 dagger 所需要?jiǎng)?chuàng)建的類(lèi)
mPresenter = new LoginPresenter(this, new LoginModelImpl(helper, retrofit));
從這句代碼就可以看出 LoginPresenter依賴于兩個(gè)對(duì)象,一個(gè)是 View 接口,另一個(gè)是 LoginModel 接口,修改 LogingPresenter:

public class LoginPresenter extends BasePresenter implement LoginContract.Presenter { 
  private LoginContract.View mView;
  private LoginModel mModel;

  @Inject
  public LoginPresenter(LoginContract.View view, LoginModel model) {
    mView = view;
    mModel = model;
  } 
  // 接口定義的方法,也不是模板能解決的
  public void loginIn(String account, String password) { // 具體的實(shí)現(xiàn)代碼 }
}

這里給 LoginPresenter 的構(gòu)造函數(shù)添加 @Inject 注解,這樣 dagger 就能判斷這是一個(gè)可用依賴注入實(shí)例化的目標(biāo)

接下來(lái),LoginPresenter 又有進(jìn)一步的依賴,由于傳入的參數(shù)都是接口,是不可能用 @Inject 標(biāo)注在構(gòu)造函數(shù)的了,所以這里又需要 dagger 中的Module提供實(shí)現(xiàn)類(lèi)的對(duì)象,本著 m 層和 v 層分離的原則,這里就需要兩個(gè) Module

@Module
public class LoginViewModule {

    LoginContract.View view;

    public LoginViewModule(LoginContract.View view) {
      this.view = view;
    }
    
    @Provide
    public LoginContract.View provideLoginView() {
      return view;
    }
}

@Module
public class LoginModelModule {
  Context context;
  public LoginModelModule(Context context) {
    this.context = context;
  }

  @Provide
  public LoginModel provideLoginModel() {
    // 省略DaoMaster.DevOpenHelper 和 Retrofit 的實(shí)例化 
    ....
    return new LoginModelImpl(helper, retrofit);
  }
}

最后就是 Component 注入器了

@Component(dependencies = {LoginModelModule.class, LoginViewModule.class})
public interface LoginPresenterComponent {
  void inject(LoginActivity activity);
}

到這里 dagger 部分的代碼也就完了,下面開(kāi)始編寫(xiě)自定義模板,這里列舉一下所有需要的模板代碼

  • Contract 類(lèi)
package ${packageName}.contract;

public interface ${businessName}Contract {
  interface View {}
  interface Presenter{}
}
  • Presneter 實(shí)現(xiàn)類(lèi)
package ${modulePackageName}.presenter;

import ${modulePackageName}.contract.${businessName}Contract;
import ${modulePackageName}.model.event.${businessName}Model;
import ${parentPresenterPackage}.${basePresenterClassName};
import javax.inject.Inject;

public class ${businessName}Presenter extends ${basePresenterClassName} implements ${businessName}Contract.Presenter {
  private ${businessName}Contract.View view;
  private ${businessName}Model model;

  @Inject
  public ${businessName}Presenter(${businessName}Contract.View view, ${businessName}Model model) {
    this.view = view;
    this.model = model;
  }
}
  • Model 接口
package ${packageName}.model.event;

public interface ${businessName}Model {}
  • Model 實(shí)現(xiàn)類(lèi)
package ${packageName}.model.impl;

import ${packageName}.model.event.${businessName}Model;
import ${daoPackage}.DaoMaster;
import ${projectPackage}.model.impl.${baseModelClassName};
import retrofit2.Retrofit;

public class ${businessName}ModelImpl extends ${baseModelClassName} implements ${businessName}Model {
  public ${businessName}ModelImpl(DaoMaster.DevOpenHelper dataBaseHelper, Retrofit retrofit) {
    super(dataBaseHelper, retrofit);
  }
}
  • Model 接口 的 Module
package ${packageName}.di.module.model;

import android.content.Context;
import ${daoPackage}.DaoMaster;
import ${packageName}.model.event.${businessName}Model;
import ${packageName}.model.impl.${businessName}ModelImpl;
import retrofit2.Retrofit;
import dagger.Module;
import dagger.Provides;

@Module
public class ${businessName}ModelModule {
  Context context;
  public ${businessName}ModelModule(Context context) { 
    this.context = context; 
  } 
  @Provides
  public ${businessName}Model provide${businessName}Model() {
    // 省略DaoMaster.DevOpenHelper 和 Retrofit 的實(shí)例化  
    .... 
    return new ${businessName}ModelImpl(helper, retrofit); 
  }
}
  • View層接口的 Module
package ${packageName}.di.module.view;

import ${packageName}.contract.${businessName}Contract;
import dagger.Module;
import dagger.Provides;

@Module
public class ${businessName}ViewModule {

    ${businessName}Contract.View view;

    public ${businessName}ViewModule(${businessName}Contract.View view) {
      this.view = view;
    }
    
    @Provide
    public ${businessName}Contract.View provide${businessName}View() {
      return view;
    }
}
  • Presenter實(shí)例的注入器
package ${packageName}.di.component.presenter;

import ${packageName}.di.module.view.${businessName}ViewModule;
import @{packageName}.di.module.model.${businessName}ModelModule;
import dagger.Component;

@Component(dependencies = {${businessName}ViewModule.class, ${businessName}ModelModule.class})
public interface ${businessName}PresenterComponent {
  void inject(t target);
}

模板代碼準(zhǔn)備好之后就可以開(kāi)始制作模板了
首先復(fù)制整個(gè) Empty Activity模板(推薦復(fù)制過(guò)來(lái)再修改的方式)

由于這個(gè)模板不涉及 activity 和manifest.xml 以及 layout,所以先刪掉相關(guān)的標(biāo)簽
先從template.xml開(kāi)始,刪掉沒(méi)用的 parameter,留下一個(gè) packageName,然后添加一個(gè)業(yè)務(wù)名稱,有這兩個(gè)就夠了

接著是設(shè)置 globals,設(shè)置一個(gè) srcOut
<global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />

最后是配置 recipe.xml.ftl, 根據(jù)自己想要的包修改一下路徑即可, 只是簡(jiǎn)單的復(fù)制工作而已了

<?xml version="1.0"?>
<recipe>
    <instantiate from="root/src/app_package/BusinessContract.java.ftl"
               to="${escapeXmlAttribute(srcOut)}/contract/${businessName}Contract.java" />

    <instantiate from="root/src/app_package/BusinessModel.java.ftl"
              to="${escapeXmlAttribute(srcOut)}/model/event/${businessName}Model.java" />

    <instantiate from="root/src/app_package/BusinessModelImpl.java.ftl"
             to="${escapeXmlAttribute(srcOut)}/model/impl/${businessName}ModelImpl.java" />

    <instantiate from="root/src/app_package/BusinessModelModule.java.ftl"
              to="${escapeXmlAttribute(srcOut)}/di/module/model/${businessName}ModelModule.java" />

    <instantiate from="root/src/app_package/BusinessViewModule.java.ftl"
              to="${escapeXmlAttribute(srcOut)}/di/module/view/${businessName}ViewModule.java"/>

    <instantiate from="root/src/app_package/BusinessPresenter.java.ftl"
              to="${escapeXmlAttribute(srcOut)}/presenter/${businessName}Presenter.java"/>

    <instantiate from="root/src/app_package/BusinessPresenterComponent.java.ftl"
              to="${escapeXmlAttribute(srcOut)}/di/component/presenter/${businessName}PresenterComponent.java"/>
</recipe>

5.遇到的一些坑

  • 1.模板一旦有錯(cuò),as 跑起來(lái)就跪了,窗口關(guān)不掉只能強(qiáng)行關(guān)閉 as 再開(kāi)過(guò)
  • 2.前面提到要留下這個(gè) packageName 本來(lái)想做成固定路徑的,但不是報(bào)錯(cuò)就是路徑不對(duì).另外不指定這個(gè) id 就沒(méi)辦法弄到當(dāng)前的路徑和包,不知道是為啥
  • 3.這個(gè)是網(wǎng)上搜索的時(shí)候看到的,貌似自定義的模板會(huì)造成as 升級(jí)失敗,如果遇到,把這份模板剪切出來(lái),升級(jí)結(jié)束后再?gòu)?fù)制回去即可
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容