当前位置: 首页 > news >正文

网站后缀是xyz指得是什么装修公司加盟费多少

网站后缀是xyz指得是什么,装修公司加盟费多少,深圳罗湖网站设计公司,阿里云 iis 默认网站Android Tencent Shadow 插件接入指南 插件化简述一、clone 仓库二、编译运行官方demo三、发布Shadow到我们本地仓库3.1、安装Nexus 3.x版本3.2、修改发布配置3.3、发布仓库3.4、引用仓库包 四、编写我们自己的代码4.1、新建项目导入maven等共同配置4.1.1、导入buildScript4.1.… Android Tencent Shadow 插件接入指南 插件化简述一、clone 仓库二、编译运行官方demo三、发布Shadow到我们本地仓库3.1、安装Nexus 3.x版本3.2、修改发布配置3.3、发布仓库3.4、引用仓库包 四、编写我们自己的代码4.1、新建项目导入maven等共同配置4.1.1、导入buildScript4.1.2、修改gradle版本和插件包版本4.1.3、添加maven依赖 4.2、实现宿主模块4.2.1、添加依赖4.2.2、撸码 4.3、静态参数 constant 的module编写4.4、plugin-loader模块实现4.5、plugin-manager的实现4.6、plugin-runtime实现4.7、插件项目的实现4.8、宿主调用插件Activity4.9、启动项目4.10、运行看看新效果吧 五、踩坑5.1、插件项目的清单文件中一定要添加package5.2、每个插件项目的build.gradle中都要引用插件5.3、每个插件都要添加依赖5.4、注意gradle版本官方推荐5.5、注意修改每个build.gradle中的版本号5.6、注意修改我文中没有提到的包名5.7、官方建议5.8、几个插件就要新建一个service并且进行不能再统一进程5.9、注意包名不要错和尽量partKey和module名保持一致这样可以减少容错率5.10、注意看文中的每一个注释代码 六、写在最后 插件化简述 关于插件化的概念就不提了大家可以自行百度 插件化难点痛点在于AndroidManifest.xml清单文件如果不是清单文件注册其实就不需要什么高深技术了可以通过反射等等方式实现很简单。因此市场上很多插件化的框架都是通过占位去实现的但是由于Android 9.0后私有API权限的问题基本市场上的框架都停止了对其的维护大家可以搜一搜基本都是只支持到9.0。那么问题来了仅仅只支持到9.0怎么够呢Android已经13了马上就14了所以今天给大家介绍下shadow的接入。他的介绍啊简述啊我就不重复复制了给大家甩几个网址 Shadow作者解析:https://juejin.cn/user/536217405890903/posts 别人录制的B站视频https://www.bilibili.com/video/BV1u14y1f7v8/?spm_id_from333.337.search-card.all.clickvd_source0f8f0025ace2f1265a86bba19aa4778d 别人写的博客 https://www.jianshu.com/p/f00dc837227f 本文demohttps://github.com/fzkf9225/shadow-plugin-master 一、clone 仓库 首先我们克隆官方仓库代码 https://github.com/Tencent/Shadow 二、编译运行官方demo 在 Shadow 目录下使用命令, 确保在 Java8 环境 ./gradlew build或者直接点击他们项目根目录下的脚本文件 gradlew.bat三、发布Shadow到我们本地仓库 3.1、安装Nexus 3.x版本 大家可以下载一个nexus具体操作可以自行百度搞定它,3.x Nexus很无脑的记得配置https证书和端口https://help.sonatype.com/repomanager3/product-information/download 3.2、修改发布配置 修改Shadow框架中的maven.gradle和common.gradle配置在根目录下的buildScript中。其中有github相关配置可改可不改不重要主要是修改如下部分因为源码都有所以直接截图了 可以先在 buildScripts/gradle/common.gradle 路径下修改发布版本或其他信息 ext.ARTIFACT_VERSION ext.VERSION_NAME ext.VERSION_SUFFIX直接修改为你想要的版本号 ext.ARTIFACT_VERSION 1.03.3、发布仓库 使用命令或者自行百度如何发布到nexus的maven仓库 ./gradlew publish3.4、引用仓库包 发布成功后即可在Shadow框架目录下找到pom文件如图 怕有人不知道怎么用我多说一句 在需要引用的build.gradle中加入 ##对应上图 implementation groupId:artifactId:version示例 implementation com.tencent.shadow.core:common:1.0为了防止回头升级导致的大量修改版本号我们可以新增一个全局配置shadow_version注意系统导包的时候是单引号我们引用参数需要改为双引号 #错误 implementation com.tencent.shadow.core:common:$shadow_version #正确 implementation com.tencent.shadow.core:common:$shadow_version四、编写我们自己的代码 4.1、新建项目导入maven等共同配置 项目结构如图仅供参考 app包名推荐com.域名.项目名示例com.casic.titan.shadowcommon没啥好解释的基础封装module而已constant静态参数module其实你可以不用但是好几个module都需要各自写魔法值容易错不如加个moduleplugin-app包名推荐com.域名.项目名示例com.casic.titan.shadow为了演示官方推荐和素质包名一致pliugin-loader推荐com.域名.loader示例com.casic.titan.loaderplugin-manager推荐com.域名.manager示例com.casic.titan.managerplugin-runtime推荐com.域名.runtime示例com.casic.titan.runtimeplugin-user推荐com.域名.随意示例com.casic.titan.user记得再gradle中加上脚本配置后面会提到 4.1.1、导入buildScript 复制shadow项目下的buildScript目录中的common.gradle和version.gradle文件其实也可以不复制但是添加公共的模块会节省很多事情复制后我们稍作修改 common.gradle如下 def gitShortRev() {def gitCommit def proc git rev-parse --short HEAD.execute()proc.in.eachLine { line - gitCommit line }proc.err.eachLine { line - println line }proc.waitFor()return gitCommit }allprojects {ext.COMPILE_SDK_VERSION 31ext.MIN_SDK_VERSION 24ext.TARGET_SDK_VERSION 31ext.VERSION_CODE 1if (${System.env.CI}.equalsIgnoreCase(true)) {ext.VERSION_NAME System.getenv(GITHUB_REF_SLUG)} else {ext.VERSION_NAME local}//System.env是获取window环境变量不用管他if (${System.env.PUBLISH_RELEASE}.equalsIgnoreCase(true)) {ext.VERSION_SUFFIX } else if (${System.env.CI}.equalsIgnoreCase(true)) {ext.VERSION_SUFFIX -${System.env.GITHUB_SHA_SHORT}-SNAPSHOT} else {ext.VERSION_SUFFIX -${gitShortRev()}-SNAPSHOT}ext.ARTIFACT_VERSION ext.VERSION_NAME ext.VERSION_SUFFIXext.TEST_HOST_APP_APPLICATION_ID com.casic.titan.test.shadow//你测试的applicationIdext.SAMPLE_HOST_APP_APPLICATION_ID com.casic.titan.shadow//你项目的applicationId }version.gradle如下 constraintlayoutVersion2.1.3 materialVersion1.5.0 appcompatVersion1.4.1 espressoCoreVersion3.4.0 shadow_version1.0 compileSdk31 minSdk24 targetSdk31 javassist_version3.28.0-GA4.1.2、修改gradle版本和插件包版本 现在新版Android Studio新建的项目默认都是8.0版本了但是Shadow不支持我们需要修改一下 a修改项目根目录下的builde.gradle plugins {id com.android.application version 7.0.3 apply false }b) gradle-wrapper.properties文件 distributionBaseGRADLE_USER_HOME distributionPathwrapper/dists distributionUrlhttps\://services.gradle.org/distributions/gradle-7.0.2-bin.zip zipStoreBaseGRADLE_USER_HOME zipStorePathwrapper/dists4.1.3、添加maven依赖 打开setting.gradle文件 pluginManagement {repositories {maven {setUrl(https://localhost:9224/repository/casic_group/)}maven { setUrl(https://mirrors.tencent.com/nexus/repository/maven-public/) }google()mavenLocal()mavenCentral()gradlePluginPortal()} } dependencyResolutionManagement {repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)repositories {maven {setUrl(https://localhost:9224/repository/casic_group/)}maven { setUrl(https://mirrors.tencent.com/nexus/repository/maven-public/) }google()mavenLocal()mavenCentral()} }尤其注意上述的几个一定要添加 mavenLocal()其中https://localhost:9224/repository/casic_group/是我本机的地址你们用不了改为自己的一定是https端口 4.2、实现宿主模块 4.2.1、添加依赖 在build.gradle下android 目录节点下添加 sourceSets {debug {assets.srcDir(build/generated/assets/plugin-manager/debug/)assets.srcDir(build/generated/assets/plugin-zip/debug/)}release {assets.srcDir(build/generated/assets/plugin-manager/release/)assets.srcDir(build/generated/assets/plugin-zip/release/)}}dataBinding{enabled true}lintOptions {checkReleaseBuilds falseabortOnError false}添加依赖包 implementation project(:constant)implementation com.tencent.shadow.core:common:$shadow_version//AndroidLogLoggerFactoryimplementation commons-io:commons-io:2.9.0//sample-host从assets中复制插件用的implementation com.tencent.shadow.dynamic:host:$shadow_version//腾讯插件框架shadowimplementation com.tencent.shadow.dynamic:host-multi-loader-ext:$shadow_version//腾讯插件框架shadow记得将compileSdk、minSdk等等修改一些统一版本号此处可忽略 compileSdk project.COMPILE_SDK_VERSIONdefaultConfig {applicationId 你的applicationIdminSdk project.MIN_SDK_VERSIONtargetSdk project.TARGET_SDK_VERSIONversionCode 1versionName 1.0multiDexEnabled truetestInstrumentationRunner androidx.test.runner.AndroidJUnitRunner}依赖库的版本也记得修改一下注意单双引号的问题此处也可忽略根据需求 implementation androidx.appcompat:appcompat:$appcompatVersionimplementation com.google.android.material:material:$materialVersiontestImplementation junit:junit:4.13.2androidTestImplementation androidx.test.ext:junit:1.1.5androidTestImplementation androidx.test.espresso:espresso-core:$espressoCoreVersion4.2.2、撸码 新建Application记得配置到清单文件中去 public class MyApplication extends Application {private static MyApplication sApp;private PluginManager mPluginManager;Overridepublic void onCreate() {super.onCreate();sApp this;detectNonSdkApiUsageOnAndroidP();setWebViewDataDirectorySuffix();LoggerFactory.setILoggerFactory(new AndroidLoggerFactory());if (isProcess(this, :plugin)) {//在全动态架构中Activity组件没有打包在宿主而是位于被动态加载的runtime//为了防止插件crash后系统自动恢复crash前的Activity组件此时由于没有加载runtime而发生classNotFound异常导致二次crash//因此这里恢复加载上一次的runtimeDynamicRuntime.recoveryRuntime(this);}if (isProcess(this, getPackageName())) {PluginHelper.getInstance().init(this);}// HostUiLayerProvider.init(this);}private static void setWebViewDataDirectorySuffix() {if (Build.VERSION.SDK_INT Build.VERSION_CODES.P) {return;}WebView.setDataDirectorySuffix(Application.getProcessName());}private static void detectNonSdkApiUsageOnAndroidP() {if (Build.VERSION.SDK_INT Build.VERSION_CODES.P) {return;}StrictMode.VmPolicy.Builder builder new StrictMode.VmPolicy.Builder();builder.detectNonSdkApiUsage();StrictMode.setVmPolicy(builder.build());}public static MyApplication getApp() {return sApp;}public void loadPluginManager(File apk) {if (mPluginManager null) {mPluginManager Shadow.getPluginManager(apk);}}public PluginManager getPluginManager() {return mPluginManager;}private static boolean isProcess(Context context, String processName) {String currentProcName ;ActivityManager manager (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {if (processInfo.pid myPid()) {currentProcName processInfo.processName;break;}}return currentProcName.endsWith(processName);} } 新建插件帮助类PluginHelper public class PluginHelper {private static final Logger mLogger LoggerFactory.getLogger(PluginHelper.class);public final static String sPluginManagerName plugin-manager.apk;//动态加载的插件管理apk/*** 动态加载的插件包里面包含以下几个部分插件apk插件框架apkloader apk和runtime apk, apk信息配置关系json文件*/public final static String sPluginZip DEBUG ? plugin-debug.zip : plugin-release.zip;public File pluginManagerFile;public File pluginZipFile;public ExecutorService singlePool Executors.newSingleThreadExecutor();private Context mContext;private static PluginHelper sInstance new PluginHelper();public static PluginHelper getInstance() {return sInstance;}private PluginHelper() {}public void init(Context context) {pluginManagerFile new File(context.getFilesDir(), sPluginManagerName);pluginZipFile new File(context.getFilesDir(), sPluginZip);mLogger.debug(pluginManagerFile:pluginManagerFile);mLogger.debug(pluginZipFile:pluginZipFile);mContext context.getApplicationContext();singlePool.execute(() - preparePlugin());}private void preparePlugin() {try {InputStream is mContext.getAssets().open(sPluginManagerName);FileUtils.copyInputStreamToFile(is, pluginManagerFile);InputStream zip mContext.getAssets().open(sPluginZip);FileUtils.copyInputStreamToFile(zip, pluginZipFile);} catch (IOException e) {throw new RuntimeException(从assets中复制apk出错, e);}} }新建日志类AndroidLoggerFactory这玩意没啥修改的直接copy源码里的吧太长了 新建shadow类 public class Shadow {public static PluginManager getPluginManager(File apk){final FixedPathPmUpdater fixedPathPmUpdater new FixedPathPmUpdater(apk);File tempPm fixedPathPmUpdater.getLatest();if (tempPm ! null) {return new DynamicPluginManager(fixedPathPmUpdater);}return null;} } 新建FixedPathPmUpdater import com.tencent.shadow.dynamic.host.PluginManagerUpdater;import java.io.File; import java.util.concurrent.Future;public class FixedPathPmUpdater implements PluginManagerUpdater {final private File apk;public FixedPathPmUpdater(File apk) {this.apk apk;}/*** return codetrue/code表示之前更新过程中意外中断了*/Overridepublic boolean wasUpdating() {return false;}/*** 更新** return 当前最新的PluginManager可能是之前已经返回过的文件但它是最新的了。*/Overridepublic FutureFile update() {return null;}/*** 获取本地最新可用的** return codenull/code表示本地没有可用的*/Overridepublic File getLatest() {return apk;}/*** 查询是否可用** param file PluginManagerUpdater返回的file* return codetrue/code表示可用codefalse/code表示不可用*/Overridepublic FutureBoolean isAvailable(final File file) {return null;} }新建service服务一个service代表一个插件你几个插件就新建几个因为我写了两个插件因此我写了两个service import com.tencent.shadow.dynamic.host.PluginProcessService;/*** 一个PluginProcessService简称PPS代表一个插件进程。插件进程由PPS启动触发启动。* 新建PPS子类允许一个宿主中有多个互不影响的插件进程。*/ public class MainPluginProcessService extends PluginProcessService { } import com.tencent.shadow.dynamic.host.PluginProcessService;/*** 一个PluginProcessService简称PPS代表一个插件进程。插件进程由PPS启动触发启动。* 新建PPS子类允许一个宿主中有多个互不影响的插件进程。*/ public class UserPluginProcessService extends PluginProcessService { } 接下来就是配置清单文件了 ?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/androidapplicationandroid:allowBackuptrueandroid:iconmipmap/ic_launcherandroid:labelstring/app_nameandroid:roundIconmipmap/ic_launcher_roundandroid:supportsRtltrueandroid:usesCleartextTraffictrueandroid:name.base.MyApplicationandroid:themestyle/Theme.Shadowpluginmasteractivityandroid:name.MainActivityandroid:exportedtrueintent-filteraction android:nameandroid.intent.action.MAIN /category android:nameandroid.intent.category.LAUNCHER //intent-filter/activity!--container 注册注意configChanges需要全注册theme需要注册成透明这些类不打包在host中打包在runtime中以便减少宿主方法数增量Activity 路径需要和插件中的匹配后面会说到--!--这三个Activity的报名不是当前的报名而是module runtime下的这个后面会说到这样写就完事了--activityandroid:namecom.casic.titan.plugin_runtime.PluginDefaultProxyActivityandroid:launchModestandardandroid:screenOrientationportraitandroid:configChangesmcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirectionandroid:hardwareAcceleratedtrueandroid:themestyle/PluginContainerActivityandroid:exportedtrueandroid:multiprocesstrue /activityandroid:namecom.casic.titan.plugin_runtime.PluginSingleInstance1ProxyActivityandroid:launchModesingleInstanceandroid:screenOrientationportraitandroid:configChangesmcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirectionandroid:hardwareAcceleratedtrueandroid:exportedtrueandroid:themestyle/PluginContainerActivityandroid:multiprocesstrue /activityandroid:namecom.casic.titan.plugin_runtime.PluginSingleTask1ProxyActivityandroid:launchModesingleTaskandroid:screenOrientationportraitandroid:exportedtrueandroid:configChangesmcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirectionandroid:hardwareAcceleratedtrueandroid:themestyle/PluginContainerActivityandroid:multiprocesstrue /providerandroid:authorities${applicationId}.contentprovider.authority.dynamicandroid:exportedtrueandroid:namecom.tencent.shadow.core.runtime.container.PluginContainerContentProviderandroid:grantUriPermissionstrueandroid:process:plugin /!--记得修改进程不同插件不能运行在同一进程不然数据肯定泄露了--serviceandroid:name.plugin_manager.MainPluginProcessServiceandroid:exportedtrueandroid:process:plugin /!--记得修改进程不同插件不能运行在同一进程不然数据肯定泄露了--serviceandroid:name.plugin_manager.UserPluginProcessServiceandroid:exportedtrueandroid:process:plugin_user //application/manifest主题PluginContainerActivity style namePluginContainerActivity parentandroid:style/Theme.NoTitleBar.Fullscreenitem nameandroid:windowBackgroundandroid:color/transparent/itemitem nameandroid:windowContentOverlaynull/itemitem nameandroid:windowNoTitletrue/itemitem nameandroid:windowIsTranslucenttrue/item/style4.3、静态参数 constant 的module编写 直接新建一个类 final public class Constant {public static final String KEY_PLUGIN_ZIP_PATH pluginZipPath;public static final String KEY_ACTIVITY_CLASSNAME KEY_ACTIVITY_CLASSNAME;public static final String KEY_EXTRAS KEY_EXTRAS;public static final String KEY_PLUGIN_NAME key_plugin_name;public static final String KEY_PLUGIN_PART_KEY KEY_PLUGIN_PART_KEY;public static final String PLUGIN_APP_NAME plugin-app;//建议者两个参数一致也和module名保持一致建议但非必须public static final String PART_KEY_PLUGIN_BASE plugin-app; //part-key 和 plugin-app build.gradle中一致public static final String PLUGIN_USER_NAME plugin-user;//建议者两个参数一致也和module名保持一致建议但非必须public static final String PART_KEY_PLUGIN_USER plugin-user; //part-key 和 plugin-app build.gradle中一致public static final int FROM_ID_NOOP 1000;public static final long FROM_ID_START_ACTIVITY 1002;//标识启动的是Activitypublic static final int FROM_ID_CALL_SERVICE 1001;//标识启动的是Servicepublic static final int FROM_ID_CLOSE 1003;public static final int FROM_ID_LOAD_VIEW_TO_HOST 1004;}build.grale配置 plugins { //修改为libraryid com.android.library }4.4、plugin-loader模块实现 在build.gradle中导入依赖库 //自带的可以删除只保留这几个不是一定要删随你但是你删除库会导致资源文件引用不到res下也要删还有清单文件implementation com.tencent.shadow.dynamic:loader-impl:$shadow_versioncompileOnly com.tencent.shadow.core:activity-container:$shadow_versioncompileOnly com.tencent.shadow.core:common:$shadow_versioncompileOnly com.tencent.shadow.dynamic:host:$shadow_version//下面这行依赖是为了防止在proguard的时候找不到LoaderFactory接口 新建SampleComponentManager记得修改三个activity的路径包名 import android.content.ComponentName; import android.content.Context;import com.tencent.shadow.core.loader.infos.ContainerProviderInfo; import com.tencent.shadow.core.loader.managers.ComponentManager;public class SampleComponentManager extends ComponentManager {/*** runtime 模块中定义的壳子Activity, 路径类名保持一致需要在宿主AndroidManifest.xml注册*/private static final String DEFAULT_ACTIVITY com.casic.titan.plugin_runtime.PluginDefaultProxyActivity;private static final String SINGLE_INSTANCE_ACTIVITY com.casic.titan.plugin_runtime.PluginSingleInstance1ProxyActivity;private static final String SINGLE_TASK_ACTIVITY com.casic.titan.plugin_runtime.PluginSingleTask1ProxyActivity;private Context context;public SampleComponentManager(Context context) {this.context context;}/*** 配置插件Activity 到 壳子Activity的对应关系** param pluginActivity 插件Activity* return 壳子Activity*/Overridepublic ComponentName onBindContainerActivity(ComponentName pluginActivity) {switch (pluginActivity.getClassName()) {/*** 这里配置对应的对应关系, 启动不同启动模式的Acitvity*/}return new ComponentName(context, DEFAULT_ACTIVITY);}/*** 配置对应宿主中预注册的壳子contentProvider的信息*/Overridepublic ContainerProviderInfo onBindContainerContentProvider(ComponentName pluginContentProvider) {return new ContainerProviderInfo(com.tencent.shadow.core.runtime.container.PluginContainerContentProvider,context.getPackageName() .contentprovider.authority.dynamic);} 新建SamplePluginLoader public class SamplePluginLoader extends ShadowPluginLoader {private final static String TAG shadow;private ComponentManager componentManager;public SamplePluginLoader(Context hostAppContext) {super(hostAppContext);componentManager new SampleComponentManager(hostAppContext);}Overridepublic ComponentManager getComponentManager() {return componentManager;} }新建目录这个目录一定要是这个并且不能错不能改 com.tencent.shadow.dynamic.loader.impl新建类CoreLoaderFactoryImpl类名不可改 import android.content.Context;import com.casic.titan.plugin_loader.SamplePluginLoader; import com.tencent.shadow.core.loader.ShadowPluginLoader;import org.jetbrains.annotations.NotNull;/*** 这个类的包名类名是固定的。* p* 见com.tencent.shadow.dynamic.loader.impl.DynamicPluginLoader#CORE_LOADER_FACTORY_IMPL_NAME*/ public class CoreLoaderFactoryImpl implements CoreLoaderFactory {NotNullOverridepublic ShadowPluginLoader build(NotNull Context context) {return new SamplePluginLoader(context);} } 官方demo中有WhiteList你可以不用需要用的时候再说 4.5、plugin-manager的实现 修改build.gradle依赖 implementation project(path: :constant)implementation com.tencent.shadow.dynamic:manager:$shadow_versioncompileOnly com.tencent.shadow.core:common:$shadow_versioncompileOnly com.tencent.shadow.dynamic:host:$shadow_version新建FastPluginManager import android.content.Context; import android.os.RemoteException; import android.util.Pair;import com.tencent.shadow.core.common.Logger; import com.tencent.shadow.core.common.LoggerFactory; import com.tencent.shadow.core.manager.installplugin.InstalledPlugin; import com.tencent.shadow.core.manager.installplugin.InstalledType; import com.tencent.shadow.core.manager.installplugin.PluginConfig; import com.tencent.shadow.dynamic.host.FailedException; import com.tencent.shadow.dynamic.manager.PluginManagerThatUseDynamicLoader;import org.json.JSONException;import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException;public abstract class FastPluginManager extends PluginManagerThatUseDynamicLoader {private static final Logger mLogger LoggerFactory.getLogger(FastPluginManager.class);private ExecutorService mFixedPool Executors.newFixedThreadPool(4);public FastPluginManager(Context context) {super(context);}public InstalledPlugin installPlugin(String zip, String hash, boolean odex) throws IOException, JSONException, InterruptedException, ExecutionException {final PluginConfig pluginConfig installPluginFromZip(new File(zip), hash);final String uuid pluginConfig.UUID;ListFuture futures new LinkedList();ListFuturePairString, String extractSoFutures new LinkedList();if (pluginConfig.runTime ! null pluginConfig.pluginLoader ! null) {Future odexRuntime mFixedPool.submit(new Callable() {Overridepublic Object call() throws Exception {oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_RUNTIME,pluginConfig.runTime.file);return null;}});futures.add(odexRuntime);Future odexLoader mFixedPool.submit(new Callable() {Overridepublic Object call() throws Exception {oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_LOADER,pluginConfig.pluginLoader.file);return null;}});futures.add(odexLoader);}for (Map.EntryString, PluginConfig.PluginFileInfo plugin : pluginConfig.plugins.entrySet()) {final String partKey plugin.getKey();final File apkFile plugin.getValue().file;FuturePairString, String extractSo mFixedPool.submit(() - extractSo(uuid, partKey, apkFile));futures.add(extractSo);extractSoFutures.add(extractSo);if (odex) {Future odexPlugin mFixedPool.submit(new Callable() {Overridepublic Object call() throws Exception {oDexPlugin(uuid, partKey, apkFile);return null;}});futures.add(odexPlugin);}}for (Future future : futures) {future.get();}MapString, String soDirMap new HashMap();for (FuturePairString, String future : extractSoFutures) {PairString, String pair future.get();soDirMap.put(pair.first, pair.second);}onInstallCompleted(pluginConfig, soDirMap);return getInstalledPlugins(1).get(0);}protected void callApplicationOnCreate(String partKey) throws RemoteException {Map map mPluginLoader.getLoadedPlugin();Boolean isCall (Boolean) map.get(partKey);if (isCall null || !isCall) {mPluginLoader.callApplicationOnCreate(partKey);}}private void loadPluginLoaderAndRuntime(String uuid, String partKey) throws RemoteException, TimeoutException, FailedException {if (mPpsController null) {bindPluginProcessService(getPluginProcessServiceName(partKey));waitServiceConnected(10, TimeUnit.SECONDS);}loadRunTime(uuid);loadPluginLoader(uuid);}protected void loadPlugin(String uuid, String partKey) throws RemoteException, TimeoutException, FailedException {loadPluginLoaderAndRuntime(uuid, partKey);Map map mPluginLoader.getLoadedPlugin();if (!map.containsKey(partKey)) {mPluginLoader.loadPlugin(partKey);}}protected abstract String getPluginProcessServiceName(String partKey);} 新建类SamplePluginManager public class SamplePluginManager extends FastPluginManager {private static final Logger logger LoggerFactory.getLogger(SamplePluginManager.class);private ExecutorService executorService Executors.newSingleThreadExecutor();private Context mCurrentContext;public SamplePluginManager(Context context) {super(context);mCurrentContext context;}/*** return PluginManager实现的别名用于区分不同PluginManager实现的数据存储路径*/Overrideprotected String getName() {return test-dynamic-manager;}/*** return 宿主中注册的PluginProcessService实现的类名*/Overrideprotected String getPluginProcessServiceName(String partKey) {if (PART_KEY_PLUGIN_USER.equals(partKey)) {return com.casic.titan.shadow.plugin_manager.UserPluginProcessService;} else if (PART_KEY_PLUGIN_BASE.equals(partKey)) {return com.casic.titan.shadow.plugin_manager.MainPluginProcessService;}throw new RuntimeException(partKey is unknown);}Overridepublic void enter(final Context context, long fromId, Bundle bundle, final EnterCallback callback) {if (fromId Constant.FROM_ID_NOOP) {//do nothing.} else if (fromId Constant.FROM_ID_START_ACTIVITY) {onStartActivity(context, bundle, callback);} else if (fromId Constant.FROM_ID_CLOSE) {close();} else if (fromId Constant.FROM_ID_LOAD_VIEW_TO_HOST) {loadViewToHost(context, bundle);} else {throw new IllegalArgumentException(不认识的fromId fromId);}}private void loadViewToHost(final Context context, Bundle bundle) {Intent pluginIntent new Intent();pluginIntent.setClassName(context.getPackageName(),com.tencent.shadow.sample.plugin.app.lib.usecases.service.HostAddPluginViewService);pluginIntent.putExtras(bundle);try {mPluginLoader.startPluginService(pluginIntent);} catch (RemoteException e) {throw new RuntimeException(e);}}private void onStartActivity(final Context context, Bundle bundle, final EnterCallback callback) {final String pluginZipPath bundle.getString(Constant.KEY_PLUGIN_ZIP_PATH);final String partKey bundle.getString(Constant.KEY_PLUGIN_PART_KEY);final String className bundle.getString(Constant.KEY_ACTIVITY_CLASSNAME);logger.debug(pluginZipPath:pluginZipPath);logger.debug(className:className);if (className null) {throw new NullPointerException(className null);}final Bundle extras bundle.getBundle(Constant.KEY_EXTRAS);if (callback ! null) {final View view LayoutInflater.from(mCurrentContext).inflate(R.layout.activity_load_plugin, null);callback.onShowLoadingView(view);}executorService.execute(() - {try {InstalledPlugin installedPlugin installPlugin(pluginZipPath, null, true);loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_BASE);loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_USER);callApplicationOnCreate(PART_KEY_PLUGIN_BASE);callApplicationOnCreate(PART_KEY_PLUGIN_USER);Intent pluginIntent new Intent();pluginIntent.setClassName(context.getPackageName(),className);if (extras ! null) {pluginIntent.replaceExtras(extras);}Intent intent mPluginLoader.convertActivityIntent(pluginIntent);intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);mPluginLoader.startActivityInPluginProcess(intent);} catch (Exception e) {throw new RuntimeException(e);}if (callback ! null) {callback.onCloseLoadingView();}});} } 修改上述SamplePluginManager 类代码 /*** return 宿主中注册的PluginProcessService实现的类名*/Overrideprotected String getPluginProcessServiceName(String partKey) {if (PART_KEY_PLUGIN_USER.equals(partKey)) {//与你partKey和Service对应起来修改为自己的报名和静态参数return com.casic.titan.shadow.plugin_manager.UserPluginProcessService;} else if (PART_KEY_PLUGIN_BASE.equals(partKey)) {//与你partKey和Service对应起来修改为自己的报名和静态参数return com.casic.titan.shadow.plugin_manager.MainPluginProcessService;}throw new RuntimeException(partKey is unknown);} 修改SamplePluginManager 类方法onStartActivity //两个插件这样写loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_BASE);loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_USER);callApplicationOnCreate(PART_KEY_PLUGIN_BASE);callApplicationOnCreate(PART_KEY_PLUGIN_USER);//一个插件就删掉一个这样写loadPlugin(installedPlugin.UUID, PART_KEY_PLUGIN_BASE);callApplicationOnCreate(PART_KEY_PLUGIN_BASE);新建包名不可修改不能错 com.tencent.shadow.dynamic.impl路径下新建ManagerFactoryImpl类型不可修改 import android.content.Context;import com.casic.titan.plugin_manager.SamplePluginManager; import com.tencent.shadow.dynamic.host.ManagerFactory; import com.tencent.shadow.dynamic.host.PluginManagerImpl;/*** 此类包名及类名固定*/ public final class ManagerFactoryImpl implements ManagerFactory {Overridepublic PluginManagerImpl buildManager(Context context) {return new SamplePluginManager(context);} } 4.6、plugin-runtime实现 添加依赖包 implementation com.tencent.shadow.core:activity-container:$shadow_version//其他的包可以删除当然你删除后也一定要删除同步的res和清单文件不然会报错也可以不删新建三个Activity不用再清单文件注册这就是宿主清单文件中注册的Activity其实这里是用来加载你插件中的Activity因为shadow几乎重写了Activity代码 import com.tencent.shadow.core.runtime.container.PluginContainerActivity; public class PluginDefaultProxyActivity extends PluginContainerActivity { }import com.tencent.shadow.core.runtime.container.PluginContainerActivity; public class PluginSingleInstance1ProxyActivity extends PluginContainerActivity { }import com.tencent.shadow.core.runtime.container.PluginContainerActivity; public class PluginSingleTask1ProxyActivity extends PluginContainerActivity { }在清单文件上添加上package因为Android Studio新建项目和module时不会添加这个package所以你需要手动添加 packagecom.casic.titan.plugin_runtime完整的清单文件 ?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/androidpackagecom.casic.titan.plugin_runtime/manifest4.7、插件项目的实现 这里我就写一个了另外一个没区别只是代码不一样而已 添加依赖 compileOnly com.tencent.shadow.core:runtime:$shadow_version在android节点下添加 buildTypes {debug {minifyEnabled falseproguardFiles getDefaultProguardFile(proguard-android.txt), proguard-rules.pro}release {minifyEnabled falseproguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.prosigningConfig signingConfigs.create(release)signingConfig.initWith(buildTypes.debug.signingConfig)}}compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8}lintOptions {abortOnError false}// 将插件applicationId设置为和宿主相同productFlavors {plugin {applicationId project.SAMPLE_HOST_APP_APPLICATION_ID}}// 将插件的资源ID分区改为和宿主0x7F不同的值aaptOptions {additionalParameters --package-id, 0x7E, --allow-reserved-package-id}在build.gradle顶部添加一定要写在最顶部 buildscript {//这个模块要放在plugins {} 之前repositories {maven { setUrl(https://localhost:9224/repository/casic_group/) }maven { setUrl(https://mirrors.tencent.com/nexus/repository/maven-public/) }google()mavenLocal()mavenCentral()}dependencies {classpath com.tencent.shadow.core:runtime:$shadow_versionclasspath com.tencent.shadow.core:activity-container:$shadow_versionclasspath com.tencent.shadow.core:gradle-plugin:$shadow_versionclasspath org.javassist:javassist:$javassist_version} }apply plugin: com.android.application apply plugin: com.tencent.shadow.plugin在build.gradle底部最底部添加代码只用一个插件添加就可以了不用每个插件项目都添加因为这里代码仅用于测试。上面的依赖配置是每个插件项目都要加只有这里不用因为他是测试、测试、测试 shadow {transform { // useHostContext [abc]}packagePlugin {pluginTypes {debug {loaderApkConfig new Tuple2(plugin-loader-debug.apk, :plugin-loader:assembleDebug)//其中plugin-loader是moduel的名字plugin-loaderruntimeApkConfig new Tuple2(plugin-runtime-debug.apk, :plugin-runtime:assembleDebug)pluginApks {pluginApp {businessName plugin-app//与下面保持一致最好也可以为空具体解释可以看官方的partKey plugin-app//静态参数constant 的module中的partKey我建议和项目名保持一致buildTask :plugin-app:assemblePluginDebug//这里的plugin-app是module名字因为需要调用脚本assemblePluginDebug这句话是意思是加载plugin-app下的assemblePluginDebug任务apkPath plugin-app/build/outputs/apk/plugin/debug/plugin-app-plugin-debug.apk//编译后apk的输出位置}pluginUser {//第二个插件businessName plugin-user//与下面保持一致最好也可以为空具体解释可以看官方的partKey plugin-user//静态参数constant 的module中的partKeybuildTask :plugin-user:assemblePluginDebug//这里的plugin-app是module名字因为需要调用脚本assemblePluginDebug这句话是意思是加载plugin-app下的assemblePluginDebug任务apkPath plugin-user/build/outputs/apk/plugin/debug/plugin-user-plugin-debug.apk//编译后apk的输出位置}}}release {//这个就不解释了是版本的区别loaderApkConfig new Tuple2(plugin-loader-release.apk, :plugin-loader:assembleRelease)runtimeApkConfig new Tuple2(plugin-runtime-release.apk, :plugin-runtime:assembleRelease)pluginApks {pluginApp {businessName plugin-apppartKey plugin-appbuildTask :plugin-app:assemblePluginReleaseapkPath plugin-app/build/outputs/apk/release/plugin-app-release.apk}pluginUser {businessName plugin-userpartKey plugin-userbuildTask :plugin-user:assemblePluginReleaseapkPath plugin-user/build/outputs/apk/plugin/debug/plugin-user-plugin-debug.apk}}}}loaderApkProjectPath plugin-loaderruntimeApkProjectPath plugin-runtimearchiveSuffix System.getenv(PluginSuffix) ?: archivePrefix plugindestinationDir ${getRootProject().getBuildDir()}version 1//暂时不知道干嘛的等我阅读下源码再说compactVersion [1, 2, 3]uuidNickName 1.0} }记得在清单文件中添加package一定要添加 packagecom.casic.titan.shadow其他的没啥说的了编写一个Activity用于演示 public class MainActivity extends AppCompatActivity {Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);} }?xml version1.0 encodingutf-8? androidx.constraintlayout.widget.ConstraintLayout xmlns:androidhttp://schemas.android.com/apk/res/androidxmlns:apphttp://schemas.android.com/apk/res-autoxmlns:toolshttp://schemas.android.com/toolsandroid:layout_widthmatch_parentandroid:layout_heightmatch_parenttools:context.MainActivityTextViewandroid:layout_widthwrap_contentandroid:layout_heightwrap_contentapp:layout_constraintStart_toStartOfparentapp:layout_constraintEnd_toEndOfparentapp:layout_constraintTop_toTopOfparentapp:layout_constraintBottom_toBottomOfparentandroid:text这是plugin-app插件内容哦android:textSize16sp/ /androidx.constraintlayout.widget.ConstraintLayout插件就编写好了当然写项目时不可能就这么点业务逻辑根据自己的实现吧 4.8、宿主调用插件Activity 在宿主项目中新建一个布局文件我开启了databinding所以布局不一样不用我解释把 ?xml version1.0 encodingutf-8? layout xmlns:androidhttp://schemas.android.com/apk/res/androidxmlns:apphttp://schemas.android.com/apk/res-autodata/dataandroidx.constraintlayout.widget.ConstraintLayoutandroid:layout_widthmatch_parentandroid:layout_heightmatch_parentandroid:orientationverticalButtonandroid:idid/start_plugin_appandroid:layout_widthwrap_contentandroid:layout_heightwrap_contentapp:layout_constraintStart_toStartOfparentapp:layout_constraintEnd_toEndOfparentapp:layout_constraintTop_toTopOfparentandroid:layout_marginTop16dpandroid:textAllCapsfalseandroid:onClickstart_plugin_appandroid:text启动插件plugin-app /Buttonandroid:idid/start_plugin_userandroid:layout_widthwrap_contentandroid:layout_heightwrap_contentapp:layout_constraintStart_toStartOfparentapp:layout_constraintEnd_toEndOfparentapp:layout_constraintTop_toBottomOfid/start_plugin_appandroid:onClickstart_plugin_userandroid:textAllCapsfalseandroid:text启动插件plugin-user //androidx.constraintlayout.widget.ConstraintLayout /layout 新建一个MainActivity并设置为启动页点击事件代码 public void start_plugin_app(View view) {PluginHelper.getInstance().singlePool.execute(()- {MyApplication.getApp().loadPluginManager(PluginHelper.getInstance().pluginManagerFile);/*** param context context* param formId 标识本次请求的来源位置用于区分入口* param bundle 参数列表, 建议在参数列表加入自己的验证* param callback 用于从PluginManager实现中返回View*/Bundle bundle new Bundle();//插件 zip这几个参数也都可以不传直接在 PluginManager 中硬编码bundle.putString(Constant.KEY_PLUGIN_ZIP_PATH,PluginHelper.getInstance().pluginZipFile.getAbsolutePath());bundle.putString(Constant.KEY_PLUGIN_NAME,Constant.PLUGIN_APP_NAME); // partKey 每个插件都有自己的 partKey 用来区分多个插件如何配置在下面讲到bundle.putString(Constant.KEY_ACTIVITY_CLASSNAME,com.casic.titan.shadow.MainActivity); //要启动的插件的Activity页面bundle.putBundle(Constant.KEY_EXTRAS, new Bundle()) ;// 要传入到插件里的参数MyApplication.getApp().getPluginManager().enter(this,Constant.FROM_ID_START_ACTIVITY,bundle,new EnterCallback() {Overridepublic void onShowLoadingView(View view) {}Overridepublic void onCloseLoadingView() {}Overridepublic void onEnterComplete() {}});});}启动plugin-user插件点击事件代码 public void start_plugin_user(View view) {PluginHelper.getInstance().singlePool.execute(()- {MyApplication.getApp().loadPluginManager(PluginHelper.getInstance().pluginManagerFile);/*** param context context* param formId 标识本次请求的来源位置用于区分入口* param bundle 参数列表, 建议在参数列表加入自己的验证* param callback 用于从PluginManager实现中返回View*/Bundle bundle new Bundle();//插件 zip这几个参数也都可以不传直接在 PluginManager 中硬编码bundle.putString(Constant.KEY_PLUGIN_ZIP_PATH,PluginHelper.getInstance().pluginZipFile.getAbsolutePath());bundle.putString(Constant.KEY_PLUGIN_NAME,Constant.PLUGIN_USER_NAME); // partKey 每个插件都有自己的 partKey 用来区分多个插件如何配置在下面讲到bundle.putString(Constant.KEY_ACTIVITY_CLASSNAME,com.casic.titan.user.LoginActivity//插件中的Activity全路径); //要启动的插件的Activity页面bundle.putBundle(Constant.KEY_EXTRAS, new Bundle()) ;// 要传入到插件里的参数MyApplication.getApp().getPluginManager().enter(this,Constant.FROM_ID_START_ACTIVITY,bundle,new EnterCallback() {Overridepublic void onShowLoadingView(View view) {}Overridepublic void onCloseLoadingView() {}Overridepublic void onEnterComplete() {}});});}4.9、启动项目 在启动项目前需要对插件进行打包在宿主module下的build.gradle最底部添加代码 def createCopyTask(projectName, buildType, name, apkName, inputFile, taskName) {def outputFile file(${getBuildDir()}/generated/assets/${name}/${buildType}/${apkName})outputFile.getParentFile().mkdirs()return tasks.create(copy${buildType.capitalize()}${name.capitalize()}Task, Copy) {group builddescription 复制${name}到assets中.from(inputFile.getParent()) {include(inputFile.name)rename { outputFile.name }}into(outputFile.getParent())}.dependsOn(${projectName}:${taskName}) }def generateAssets(generateAssetsTask, buildType) {def moduleName plugin-managerdef pluginManagerApkFile file(${project(:plugin-manager).getBuildDir()} /outputs/apk/${buildType}/ ${moduleName}-${buildType}.apk)generateAssetsTask.dependsOn createCopyTask(:plugin-manager,//项目名buildType,moduleName,plugin-manager.apk,//plugin-manager module打包后的apk名字我建议是module名保持一致此出名字要和宿主类PluginHelper中的sPluginManagerName参数保持一致pluginManagerApkFile,assemble${buildType.capitalize()})def pluginZip file(${getRootProject().getBuildDir()}/plugin-${buildType}.zip)generateAssetsTask.dependsOn createCopyTask(:plugin-app,//这里至于为什么写plugin-app具体暂时还没摸明白反正这么写可以等我再研究下源码buildType,plugin-zip,plugin-${buildType}.zip,pluginZip,package${buildType.capitalize()}Plugin) }tasks.whenTaskAdded { task -if (task.name generateDebugAssets) {generateAssets(task, debug)}if (task.name generateReleaseAssets) {generateAssets(task, release)} }4.10、运行看看新效果吧 五、踩坑 5.1、插件项目的清单文件中一定要添加package 5.2、每个插件项目的build.gradle中都要引用插件 apply plugin: com.tencent.shadow.plugin并且添加classpath在最上面当然你如果是单独app打包的话这些配置应该在项目下的build.gradle中这得看插件项目时单独开发还是在插件宿主项目下开发了。 buildscript {//这个模块要放在plugins {} repositories {//换成你自己的modulemaven { setUrl(https://localhost:9224/repository/casic_group/) }maven { setUrl(https://mirrors.tencent.com/nexus/repository/maven-public/) }google()mavenLocal()mavenCentral()}dependencies {classpath com.tencent.shadow.core:runtime:$shadow_versionclasspath com.tencent.shadow.core:activity-container:$shadow_versionclasspath com.tencent.shadow.core:gradle-plugin:$shadow_versionclasspath org.javassist:javassist:$javassist_version} }5.3、每个插件都要添加依赖 compileOnly com.tencent.shadow.core:runtime:$shadow_version5.4、注意gradle版本官方推荐 plugins {id com.android.application version 7.0.3 apply false }#Thu Aug 03 17:23:27 CST 2023 distributionBaseGRADLE_USER_HOME distributionPathwrapper/dists distributionUrlhttps\://services.gradle.org/distributions/gradle-7.0.2-bin.zip zipStoreBaseGRADLE_USER_HOME zipStorePathwrapper/dists 5.5、注意修改每个build.gradle中的版本号 例如依赖库的版本统一一下防止某一个版本过高导致报错你半天都找不到问题 implementation androidx.appcompat:appcompat:$appcompatVersionimplementation com.google.android.material:material:$materialVersiontestImplementation junit:junit:4.13.2androidTestImplementation androidx.test.ext:junit:1.1.5androidTestImplementation androidx.test.espresso:espresso-core:$espressoCoreVersion minSdk project.MIN_SDK_VERSIONtargetSdk project.TARGET_SDK_VERSION5.6、注意修改我文中没有提到的包名 com.casic.titan.shadow //这是我宿主的包名5.7、官方建议 官方建议插件包名和宿主包名一致但是大家都知道这不实际所以我写了两个插件一个是和宿主一样一个是不一样官方提供方法解决问题在插件build.gradle中android节点下添加配置 // 将插件applicationId设置为和宿主相同productFlavors {plugin {applicationId project.SAMPLE_HOST_APP_APPLICATION_ID}}5.8、几个插件就要新建一个service并且进行不能再统一进程 serviceandroid:name.plugin_manager.MainPluginProcessServiceandroid:exportedtrueandroid:process:plugin /serviceandroid:name.plugin_manager.UserPluginProcessServiceandroid:exportedtrueandroid:process:plugin_user /5.9、注意包名不要错和尽量partKey和module名保持一致这样可以减少容错率 5.10、注意看文中的每一个注释代码 六、写在最后 有问题欢迎博客留言、也欢迎在github上提issue我会尽快回复
http://www.zqtcl.cn/news/195252/

相关文章:

  • 网站建设微信运营销售做网站用啥语言
  • dw建设网站步骤活动汪活动策划网站
  • 民和县公司网站建设网站开发的特点
  • 模板企业快速建站上传网站中ftp地址写什么
  • 云南本地企业做网站太原网站制作公司哪家好
  • 西部数码域名网站模板wordpress抓取股票行情
  • 丰台深圳网站建设公司关于服装店网站建设的策划方案
  • win7 iis网站无法显示随州网站建设哪家实惠
  • 利用网站新媒体宣传法治建设建站哪个平台好
  • 网站seo课设wordpress 500 根目录
  • 电子商务网站建设的阶段化分析如何利用视频网站做数字营销推广
  • 电子商务网站建设ppt模板国外注册机网站
  • 西部数码做跳转网站百度seo排名培训优化
  • 农业网站素材wordpress all in one
  • 学习网站建设有前景没wordpress 和dokuwiki
  • 服装网站开发方案网站设计美工排版编辑
  • 旅游网站首页模板下载广州市建设工程检测中心网站
  • 餐饮加盟网站建设wordpress 首行缩进
  • kkday是哪里做的网站橙云 php网站建设
  • 站长之家0网站规划作品
  • 物流公司网站建设系统规划广告设计怎么学
  • 异地备案 网站中信建设有限责任公司经济性质
  • 网站没有备案怎么申请广告宿迁莱布拉网站建设
  • 太原适合网站设计地址网站建设 教学视频教程
  • 建商城网站需要多少钱网站开发维护报价单
  • 唐山网站建设冀icp备婚纱网站页面设计
  • 做购物网站支付需要怎么做手机网站建设教程
  • 国外网站空间租用哪个好建站快车打电话
  • 自媒体网站 程序做药公司的网站前置审批
  • 简洁网站模板素材廊坊建设企业网站