全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2024-01-09_Android一杯冰美式的时间--LayoutInflater

您的位置:首页 >> 新闻 >> 行业资讯

Android一杯冰美式的时间--LayoutInflater 点击关注公众号,”技术干货”及时达!一、前言上文【Android一杯冰美式的时间--去找setContentView】(https://juejin.cn/post/7315245519842885632),最后因为我太困了而结束在LayoutInflater。这篇文章还是要补足玩这一步的。 在 Android 应用中,界面是通过布局文件(通常是 XML 文件)来定义的。这些布局文件描述了界面的结构和外观,包括各种控件和它们的属性。但是,为了在应用运行时使用这些布局,我们需要将它们从 XML 文件转换成 Java 或 Kotlin 代码中的View对象。这就是 LayoutInflater 的作用所在。 如果你没用过LayoutInflater....当我没说。 (说完了...) 如果您有任何疑问、对文章写的不满意、发现错误、想吐槽或者有更好的想法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。?? 二、使用看看我们平常使用LayoutInflater的方法: 通过系统服务获取布局加载器LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(resource,root,attachToRoot); 通过Activity中的getLayoutInflater()方法View view = getLayoutInflater().inflate(resource,root,attachToRoot); 通过View的静态inflate()方法View view = View.inflate(resource,root,attachToRoot); 通过LayoutInflater的from()方法View view = LayoutInflater.from(this).inflate(resource,root,attachToRoot); 三、inflate最后你会发现“二”中这些用法间接或直接的调用了LayoutInflater中的静态方法: public static LayoutInflater from(@UiContext Context context) { LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); if (LayoutInflater == null) { throw new AssertionError("LayoutInflater not found."); } return LayoutInflater; } 它们使用的都是: LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); PS:系统会初始化LAYOUT_INFLATER_SERVICE服务,AssertionError("LayoutInflater not found.")几乎不会出现(反正我没看到过)。 然后它们都会调用LayoutInflater.inflate ,而它有三个重载函数 public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) { return inflate(resource, root, root != null); } public View inflate(XmlPullParser parser, @Nullable ViewGroup root) { return inflate(parser, root, root != null); } //会创建一个XmlResourceParser对象 public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { final Resources res = getContext().getResources(); View view = tryInflatePrecompiled(resource, res, root, attachToRoot); if (view != null) { return view; } XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } } public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) 最后方法都会指向inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)。我将展开/省略该方法的部分代码: public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { // ... [省略部分代码] View result = root; try { //??advanceToRootNode(parser)展开 //移动解析器到 XML 文档的根元素,找根布局 int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty } if (type != XmlPullParser.START_TAG) { throw new InflateException(parser.getPositionDescription() + ": No start tag found!"); } //??advanceToRootNode(parser)展开 final String name = parser.getName(); // merge 标签只能在 root 非 null 且 attachToRoot 为 true 的情况下使用 if (TAG_MERGE.equals(name)) { if (root == null || !attachToRoot) { throw new InflateException("merge / can only be used with a valid ViewGroup root and attachToRoot=true"); } rInflate(parser, root, inflaterContext, attrs, false); } else { // 创建根视图 // 按下不表 1?? final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; // 如果 root 非 null,则生成与 root 匹配的布局参数 if (root != null) { params = root.generateLayoutParams(attrs); // 如果不附加到 root,则先设置布局参数 if (!attachToRoot) { temp.setLayoutParams(params); } } // 递归地填充所有子视图 rInflateChildren(parser, temp, attrs, true); // 如果 root 非 null 且 attachToRoot 为 true,则将解析的视图附加到 root if (root != null && attachToRoot) { root.addView(temp, params); } // 根据 attachToRoot 决定返回哪个视图 if (root == null || !attachToRoot) { result = temp; } } } // ... [省略部分代码] return result; } } 总注释中,我们可以了解到 「@Nullable ViewGroup root」: 这个参数指定了布局文件中顶级视图的父容器。它可以是 null,表示没有父容器。当 root 非 null 时,解析出的视图可以选择性地附加到这个 root。当使用 merge 标签时,root 不能为 null,且 attachToRoot 必须为 true。「boolean attachToRoot」: 这个参数决定了解析出的视图是否应该立即附加到 root 视图组。当 attachToRoot 为 true 且 root 非 null 时,解析出的视图会被添加到 root 中。当 attachToRoot 为 false 时,即使 root 非 null,解析出的视图也不会立即添加到 root 中,而是返回这个顶级视图供后续操作。你可能还是有点迷糊,总结性的来说:在任何我们不负责将View添加进ViewGroup的情况下都应该将attachToRoot设置为false。比如RecyclerView的onCreateViewHolder。比如Fragment的onCreateView,FragmentManager 负责将 Fragment 的视图插入到容器中。如果在 onCreateView 中已经将视图附加到 root,那么当 FragmentManager 尝试再次执行这个操作时,就会引发 IllegalStateException,因为一个视图不能有多个父视图。 反之你就可以使用true值。 四、Factory2和Factory在inflate代码1??中,选择性的忽略了createViewFromTag这个方法的细节。视图如何创建出来?让我们看看最终指向的方法: View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { // ... [省略部分代码] try { //tryCreateView View view = tryCreateView(parent, name, context, attrs); if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; try { if (-1 == name.indexOf('.')) { //onCreateView view = onCreateView(context, parent, name, attrs); } else { //createView view = createView(context, name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } // ... [省略部分代码] return view; } } tryCreateView我们可以看到的是,view是否为空,直接影响着下面流程。那有必要看看tryCreateView的具体内容: public final View tryCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) { View view; if (mFactory2 != null) { view = mFactory2.onCreateView(parent, name, context, attrs); } else if (mFactory != null) { view = mFactory.onCreateView(name, context, attrs); } else { view = null; } if (view == null && mPrivateFactory != null) { view = mPrivateFactory.onCreateView(parent, name, context, attrs); } return view; } 好了,对于不太熟悉的人来说,直接懵掉。mFactory2?mFactory?先来说mFactory: private Factory mFactory; public interface Factory { //当从 LayoutInflater 加载布局时,你可以提供一个回调(hook),在布局加载过程中被调用。你可以使用这个回调来自定义你的 XML 布局文件中可用的标签名 View onCreateView(@NonNull String name, @NonNull Context context,@NonNull AttributeSet attrs); } 看起来mFactory 允许开发者提供自定义的逻辑来替代或增强标准的视图创建过程。官方??钩子! 像这样: LayoutInflater inflater = LayoutInflater.from(context); inflater.setFactory(new LayoutInflater.Factory() { @Override public View onCreateView(String name, Context context, AttributeSet attrs) { // 根据 name 创建自定义视图 if (name.equals("HarmonyOSGreateAgain")) { return new HarmonyOSView(context, attrs); } // 对于非自定义视图,返回 null 以使用默认行为 return null; } 当然你也可以这样: LayoutInflater inflater = LayoutInflater.from(context); inflater.setFactory(new LayoutInflater.Factory() { @Override public View onCreateView(String name, Context context, AttributeSet attrs) { // 根据 name 创建自定义视图 if (name.equals("TextView")) { return new HarmonyOSTextView(context, attrs); } // 对于非自定义视图,返回 null 以使用默认行为 return null; } 没错!你可以创建自定义 UI 组件或者改变标准组件的行为! 接着看看mFactory2: private Factory2 mFactory2; public interface Factory2 extends Factory { View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs); } 细心的你发现,这玩意继承自Factory,且多了一个parent参数。是的,如你想的那样,它可以对创建 View 的 Parent 进行控制。这就是它的主要目的。 你已经注意到Factory是通过setFactory设置的,那Factory2你也该猜到了。setFactory2..... 那么什么是标准组件的行为呢? onCreateView这就要看到createViewFromTag中的onCreateView和createView了。onCreateView最终指向createView,我们看看createView: //反射构造View public final View createView(@NonNull Context viewContext, @NonNull String name,@Nullable String prefix, @Nullable AttributeSet attrs) throws ClassNotFoundException, InflateException { // 确保参数不为空 Objects.requireNonNull(viewContext); Objects.requireNonNull(name); // 尝试从缓存中获取视图的构造函数 Constructor? extends View constructor = sConstructorMap.get(name); if (constructor != null && !verifyClassLoader(constructor)) { constructor = null; sConstructorMap.remove(name); } Class? extends View clazz = null; try { Trace.traceBegin(Trace.TRACE_TAG_VIEW, name); if (constructor == null) { // 如果构造函数不在缓存中,尝试加载视图类 clazz = Class.forName(prefix != null ? (prefix + name) : name, false, mContext.getClassLoader()).asSubclass(View.class); // 应用过滤器(如果有)来决定是否允许加载类 if (mFilter != null && clazz != null) { boolean allowed = mFilter.onLoadClass(clazz); if (!allowed) { failNotAllowed(name, prefix, viewContext, attrs); } } // 获取并缓存构造函数 constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); sConstructorMap.put(name, constructor); } else { // 对缓存的构造函数应用过滤器 // ... [省略过滤器逻辑] } // 设置构造函数参数并创建视图实例 Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = viewContext; Object[] args = mConstructorArgs; args[1] = attrs; try { //反射构造 final View view = constructor.newInstance(args); // 特殊处理 ViewStub // ... [省略 ViewStub 处理逻辑] return view; } finally { mConstructorArgs[0] = lastContext; } } catch (NoSuchMethodException | ClassCastException | ClassNotFoundException | Exception e) { // 处理各种异常 // ... [省略异常处理逻辑] } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } } 简单吧~总结下来就两个: 从缓存集合中获取当前View对应的构造方法,没有则创建,并存入缓存。反射构造方法,创建对应的View对象IllegalStateException当我们兴致勃勃的去准备大改特改的时候,你会发现: class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val inflater: LayoutInflater = LayoutInflater.from(this) //使用LayoutInflater.Factory inflater.factory = LayoutInflater.Factory { name, context, attrs - null } //使用LayoutInflater.Factory2 inflater.factory2 = object : LayoutInflater.Factory2 { override fun onCreateView( parent: View?, name: String, context: Context, attrs: AttributeSet ): View? { return XXX } override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? { return XXXX } } } } 两个设置方法都会崩溃~ Caused by: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater at android.view.LayoutInflater.setFactory(LayoutInflater.java:317) //和 Caused by: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater at android.view.LayoutInflater.setFactory2(LayoutInflater.java:375) 错误来自setFactory2/setFactory:(PS:setFactory2/setFactory基本一致) public void setFactory(Factory factory) { if (mFactorySet) { throw new IllegalStateException("A factory has already been set on this LayoutInflater"); } if (factory == null) { throw new NullPointerException("Given factory can not be null"); } mFactorySet = true; if (mFactory == null) { mFactory = factory; } else { mFactory = new FactoryMerger(factory, null, mFactory, mFactory2); } } public void setFactory2(Factory2 factory) { if (mFactorySet) { throw new IllegalStateException("A factory has already been set on this LayoutInflater"); } if (factory == null) { throw new NullPointerException("Given factory can not be null"); } mFactorySet = true; if (mFactory == null) { mFactory = mFactory2 = factory; } else { mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2); } } 我们可以看出,setFactory2/setFactory均只能调用一次。但是明明我们只调用了一次?为什么会抛出异常呢? AppCompatDelegate.createView我们四处寻找setFactory2/setFactory的使用者,找到了AppCompatDelegate以及它的实现类AppCompatDelegateImpl,眼熟吧!【Android一杯冰美式的时间--去找setContentView】提到的!(https://juejin.cn/post/7315245519842885632) 最终我们可以在实现类中找到 @Override public void installViewFactory() { LayoutInflater layoutInflater = LayoutInflater.from(mContext); if (layoutInflater.getFactory() == null) { LayoutInflaterCompat.setFactory2(layoutInflater, this); } else { if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) { Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed" + " so we can not install AppCompat's"); } } } 至于为什么没有setFactory的调用,你也找不到呢,因为被弃用了,而setFactory2也会 mFactory = mFactory2 = factory。相信细心的你也发现~ 我们继续对setFactory2,进行跟踪,那么我们肯定需要寻找,它的实现类。最后我们可以定位到AppCompatDelegateImpl的onCreateView-createView方法,我们和LayoutInflater中的createView对比就知道 @Override public View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) { // 检查是否已经有一个 AppCompatViewInflater 实例,如果没有,则创建一个 if (mAppCompatViewInflater == null) { TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme); String viewInflaterClassName = a.getString(R.styleable.AppCompatTheme_viewInflaterClass); if (viewInflaterClassName == null) { // 如果在主题中没有指定自定义视图创建器,则使用默认的 AppCompatViewInflater mAppCompatViewInflater = new AppCompatViewInflater(); } else { try { // 尝试通过反射加载并实例化自定义的 AppCompatViewInflater Class viewInflaterClass = mContext.getClassLoader().loadClass(viewInflaterClassName); mAppCompatViewInflater = (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor().newInstance(); } catch (Throwable t) { // 如果反射失败,回退到默认的 AppCompatViewInflater Log.i(TAG, "Failed to instantiate custom view inflater " + viewInflaterClassName + ". Falling back to default.", t); mAppCompatViewInflater = new AppCompatViewInflater(); } } } // 标记是否应该继承上下文 boolean inheritContext = false; if (IS_PRE_LOLLIPOP) { // 对于 Android Lollipop 之前的版本,检查是否需要继承上下文 if (mLayoutIncludeDetector == null) { mLayoutIncludeDetector = new LayoutIncludeDetector(); } if (mLayoutIncludeDetector.detect(attrs)) { inheritContext = true; } else { inheritContext = (attrs instanceof XmlPullParser) ? ((XmlPullParser) attrs).getDepth() 1 : shouldInheritContext((ViewParent) parent); } } // 使用 AppCompatViewInflater 创建视图 return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, // 仅在 Lollipop 之前的版本中读取 android:theme true, // 始终读取 app:theme,用于遗留原因 VectorEnabledTintResources.shouldBeUsed() // 根据配置决定是否使用着色资源 } 显然核心代码在AppCompatViewInflater.createView中。 AppCompatViewInflater.createView@Nullable public final View createView(@Nullable View parent, @NonNull final String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) { final Context originalContext = context; // 根据需要调整上下文 if (inheritContext && parent != null) { context = parent.getContext(); } if (readAndroidTheme || readAppTheme) { context = themifyContext(context, attrs, readAndroidTheme, readAppTheme); } if (wrapContext) { context = TintContextWrapper.wrap(context); } View view = null; // 根据标签名创建 AppCompat 支持的视图 switch (name) { case "TextView": view = createTextView(context, attrs); verifyNotNull(view, name); break; // ...其他视图创建逻辑 default: // 尝试使用自定义方法创建视图 view = createView(context, name, attrs); } // 如果原始上下文和调整后的上下文不同,尝试重新创建视图 if (view == null && originalContext != context) { view = createViewFromTag(context, name, attrs); } // 检查视图的 onClick 属性和无障碍属性 if (view != null) { checkOnClickListener(view, attrs); backportAccessibilityAttributes(context, view, attrs); } return view; } 在switch (name)中,返回的都是AppCompatXXX。因此,我们可以确认,默认用于确保在旧版 Android 系统上,应用也能够使用 Material Design 样式的视图,同时保持向后兼容性。也就是统一 Material Design样式。而它最后指向了AppCompatViewInflater 好了现在你已经学会使用Factory了。 值得注意的是,一般而言你需要保留AppCompatViewInflater做出的兼容操作。所以你需要如此做: LayoutInflater.from(this).setFactory2(new LayoutInflater.Factory2() { @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { // 调用 AppCompatDelegate 的createView方法 getDelegate().createView(parent, name, context, attrs); // 自由发挥 return XXX; } @Override public View onCreateView(String name, Context context, AttributeSet attrs) { return XXX; } }); 显然使用setFactory需要在加载布局前,也就是调用inflate方法之前。 五、用途你可能已经意识到了,setFactory的用途,通过 LayoutInflater 创建 View 时候的一个回调,可以通过 LayoutInflater.Factory 来改造或定制创建 View 的过程。比如样式替换,比如自定义的View等等等。这里我们展示接管View的背景绘制,你可以扩展成“无需自定义View,直接添加属性便可以实现shape、selector的效果” 以下算是一个通用操作了,也是模仿AppCompatViewInflater的流程: import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.view.LayoutInflater import androidx.appcompat.app.AppCompatActivity import androidx.core.view.LayoutInflaterCompat object BackgroundLibrary { fun inject(context: Context?): LayoutInflater? { val inflater: LayoutInflater? = if (context is Activity) { context.layoutInflater } else { LayoutInflater.from(context) } if (inflater == null) { return null } if (inflater.factory2 == null) { val factory = setDelegateFactory(context!!) inflater.factory2 = factory } else if (inflater.factory2 !is BackgroundFactory) { forceSetFactory2(inflater) } return inflater } /** * 注入自定义 LayoutInflater 工厂的主方法 * 如果因为其他库已经设置了factory,可以使用该方法去进行inject,在其他库的setFactory后面调用即可 */ fun inject2(context: Context?): LayoutInflater? { // 根据 Context 类型获取 LayoutInflater 实例 val inflater: LayoutInflater? = if (context is Activity) { context.layoutInflater } else { LayoutInflater.from(context) } if (inflater == null) { return null } // 强制设置自定义工厂 forceSetFactory2(inflater) return inflater } // 创建并配置 BackgroundFactory 实例 private fun setDelegateFactory(context: Context): BackgroundFactory { val factory = BackgroundFactory() if (context is AppCompatActivity) { // 如果是 AppCompatActivity 实例,使用其委托创建视图 val delegate = context.delegate factory.setInterceptFactory { name, context, attrs - delegate.createView(null, name, context, attrs) } } return factory } // 通过反射技术强制为 LayoutInflater 设置自定义工厂 @SuppressLint("DiscouragedPrivateApi") private fun forceSetFactory2(inflater: LayoutInflater) { val compatClass = LayoutInflaterCompat::class.java val inflaterClass = LayoutInflater::class.java try { // 访问私有字段并修改其值,以便可以设置自定义工厂 val sCheckedField = compatClass.getDeclaredField("sCheckedField").apply { isAccessible = true setBoolean(compatClass, false) } val mFactory = inflaterClass.getDeclaredField("mFactory").apply { isAccessible = true } val mFactory2 = inflaterClass.getDeclaredField("mFactory2").apply { isAccessible = true } // 创建 BackgroundFactory 实例 val factory = BackgroundFactory() if (inflater.factory2 != null) { factory.setInterceptFactory2(inflater.factory2) } else if (inflater.factory != null) { factory.setInterceptFactory(inflater.factory) } // 设置工厂到 LayoutInflater 的 mFactory 和 mFactory2 字段 mFactory2[inflater] = factory mFactory[inflater] = factory } catch (e: IllegalAccessException) { // 处理反射访问异常 e.printStackTrace() } catch (e: NoSuchFieldException) { // 处理反射访问异常 e.printStackTrace() } } } class BackgroundFactory : LayoutInflater.Factory2 { // 已经存在的工厂和工厂2的引用 private var mViewCreateFactory: LayoutInflater.Factory? = null private var mViewCreateFactory2: LayoutInflater.Factory2? = null // 用于创建视图的方法 override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? { // 检查是否为特定前缀的视图,如果是,则不处理 if (name.startsWith("com.fuck.harmonyos.view")) { return null } var view: View? = null // 首先尝试使用已经存在的工厂创建视图 if (mViewCreateFactory2 != null) { view = mViewCreateFactory2!!.onCreateView(name, context, attrs) if (view == null) { view = mViewCreateFactory2!!.onCreateView(null, name, context, attrs) } } else if (mViewCreateFactory != null) { view = mViewCreateFactory!!.onCreateView(name, context, attrs) } // 对创建的视图应用自定义背景处理 return setViewBackground(name, context, attrs, view) } // 设置拦截的工厂 fun setInterceptFactory(factory: LayoutInflater.Factory) { mViewCreateFactory = factory } fun setInterceptFactory2(factory: LayoutInflater.Factory2) { mViewCreateFactory2 = factory } // Factory2 接口的另一个方法实现 override fun onCreateView( parent: View?, name: String, context: Context, attrs: AttributeSet ): View? { return onCreateView(name, context, attrs) } // 伴生对象,包含静态方法和属性 companion object { // ... [省略静态方法和属性的注释] } } 一个BackgroundFactory,BackgroundLibrary。就可以随意组合了,具体实现可以在BackgroundFactory。慢慢琢磨。 如果是一个懒狗,肯定不乐意在每个Activity中,去添加inject的操作。所以可以直接如此做: class FuckApplication : Application() { init { registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { BackgroundLibrary.inject(activity) //BackgroundLibrary.inject2(activity) } // ... [省略部分代码] }) } } 六、 结尾部分代码可以在这里看到(https://gitee.com/no-finshing-team/article-code-repository/tree/dev-all/fuckinglayoutinflater/src/main/java/com/nofish/fuckingscroll) 感谢仓库BackgroundLibrary,该库基于LayoutInflater.Factory原理完成。 如果您有任何疑问、对文章写的不满意、发现错误、想吐槽或者有更好的想法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。?? 点击关注公众号,”技术干货”及时达! 阅读原文

上一篇:2022-05-21_「转」清华电子系数据科学与智能实验室2022年博士后招聘:城市科学与计算等多个方向 下一篇:2025-03-05_慢生产力:告别职场表演性忙碌

TAG标签:

11
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价