Lifecycle+Retrofit+Room完美结合 领略架构之美

打印 上一主题 下一主题

主题 915|帖子 915|积分 2745

}
//实现类回调方法
protected void onActive() {
}
//实现类回调方法
protected void onInactive() {
}
class LifecycleBoundObserver implements GenericLifecycleObserver {
@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(observer);
return;
}
// immediately set active state, so we’d never dispatch anything to inactive
// owner
activeStateChanged(isActiveState(owner.getLifecycle().getCurrentState()));
}
void activeStateChanged(boolean newActive) {
if (newActive == active) {
return;
}
active = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
LiveData.this.mActiveCount += active ? 1 : -1;
if (wasInactive && active) {
onActive();
}
if (LiveData.this.mActiveCount == 0 && !active) {
onInactive();
}
if (active) {//只有生命组件处于前台时,才触发数据的变更通知
dispatchingValue(this);
}
}
}
static boolean isActiveState(State state) {
return state.isAtLeast(STARTED);
}
}
看源码,会发现LiveData有个重要的方法observe(LifecycleOwner owner, Observer observer), 在数据源数据有变更时,遍历分发数据到所有监听者,最后会回调onChanged()方法。
public interface Observer {
/**


  • Called when the data is changed.
  • @param t The new data
    */
    void onChanged(@Nullable T t);
    }
LiveData有两个实现类:MediatorLiveData_和_MediatorLiveData,继承关系如下:

_MutableLiveData_类很简单,只是暴露了两个方法:postData()和setData()。 _MediatorLiveData_类有个**addSource()**方法,可以实现监听另一个或多个LiveData数据源变化,如许我们就可以比较便捷且低耦合的实现多个数据源的逻辑,并且关联到一个MediatorLiveData上,实现多数据源的自动整合。
@MainThread
public  void addSource(@NonNull LiveData source, @NonNull Observer onChanged) {
Source e = new Source<>(source, onChanged);
Source<?> existing = mSources.putIfAbsent(source, e);
if (existing != null && existing.mObserver != onChanged) {
throw new IllegalArgumentException(
“This source was already added with the different observer”);
}
if (existing != null) {
return;
}
if (hasActiveObservers()) {
e.plug();
}
}
ViewModel

LiveData和LiveCycle将数据与数据,数据与UI生命绑定到了一起,实现了数据的自动管理和更新,那这些数据怎样缓存呢?可否在多个页面共享这些数据呢?答案是ViewMode。
   A ViewModel is always created in association with a scope (an fragment or an activity) and will be retained as long as the scope is alive. E.g. if it is an Activity, until it is finished.
  ViewMode相称于一层数据隔离层,将UI层的数据逻辑全部抽离干净,管理制底层数据的获取方式和逻辑。
ViewModel viewModel = ViewModelProviders.of(this).get(xxxModel.class);
ViewModel viewModel = ViewModelProviders.of(this, factory).get(xxxModel.class);
可以通过以上方式获取ViewModel实例,如果有自界说ViewModel构造器参数,需要借助ViewModelProvider.NewInstanceFactory,自己实现create方法。
那么,ViewMode是怎么被保存的呢? 可以顺着ViewModelProviders源码进去看看。
@NonNull
@MainThread
public T get(@NonNull String key, @NonNull Class modelClass) {
ViewModel viewModel = mViewModelStore.get(key);
if (modelClass.isInstance(viewModel)) {
//noinspection unchecked
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
}
viewModel = mFactory.create(modelClass);
mViewModelStore.put(key, viewModel);
//noinspection unchecked
return (T) viewModel;
}
发现get方法会先从缓存中获取,没有的化就会通过_Factory_的create方法构造一个ViewModel,然后放入缓存,下次直接使用。
Room

Room是一种ORM(对象关系映射)模式数据库框架,对安卓SQlite的抽象封装,从此操作数据库提供了超便捷方式。
   The Room persistence library provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite.
  同样基于ORM模式封装的数据库,比较闻名尚有_GreenDao_。而Room和其他ORM对比,具有编译时验证查询语句正常性,支持LiveData数据返回等优势。 我们选择room,更多是由于对LiveData的完美支持,可以动态的将DB数据变化自动更新到LiveData上,在通过LiveData自动刷新到UI上。
这里引用网络上的一张Room与其他同类性能对比图片:

Room用法:



    • 继承RoomDatabase的抽象类, 暴露抽象方法getxxxDao()。

@Database(entities = {EssayDayEntity.class, ZhihuItemEntity.class}, version = 1)
@TypeConverters(DateConverter.class)
public abstract class AppDB extends RoomDatabase {
private static AppDB sInstance;
@VisibleForTesting
public static final String DATABASE_NAME = “canking.db”;
public abstract EssayDao essayDao();
}



    • 获取db实例

ppDatabase db = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, “database-name”).build();



    • 实现Dao层逻辑

@Dao
public interface ZhuhuDao {
@Query(“SELECT * FROM zhuhulist order by id desc, id limit 0,1”)
LiveData loadZhuhu();
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertItem(ZhihuItemEntity products);
}



    • 添加一张表结构

@Entity
public class User {
@PrimaryKey
private int uid;
@ColumnInfo(name = “first_name”)
private String firstName;
public String date;//默认columnInfo 为 date
}
就这么简单,就可以实现数据库的操作,完全隔离的底层复杂的数据库操作,大大节省项目研发重复劳动力。
从使用说明分析,UserDao和Db一个是接口,一个是抽象类,这些逻辑的实现完全是由annotationProcessor依赖注入帮我们实现的, annotationProcessor实在就是开源的android-apt的官方替换品。 那么编译项目后,可以在build目录下看到生成相应的类xxx_impl.class。

既然Room支持LiveData数据,那么有可以分析下源码,相识下详细原理,方便以后填坑。
先选Demo中Dao层的insert方法,看看数据怎样加载到内存的。我们的query方法如下:
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertItem(ZhihuItemEntity products);
annotationProcessor帮我吗生成后的实现主要代码如下:
private final RoomDatabase __db;
private final EntityInsertionAdapter __insertionAdapterOfZhihuItemEntity;
public ZhuhuDao_Impl(RoomDatabase __db) {
this.__db = __db;
//EntityInsertionAdapter类的匿名内部类实现方式,
this.__insertionAdapterOfZhihuItemEntity = new EntityInsertionAdapter(__db) {
public String createQuery() {
return “INSERT OR REPLACE INTO zhuhulist(id,date,stories,top_stories) VALUES (nullif(?, 0),?,?,?)”;
}
public void bind(SupportSQLiteStatement stmt, ZhihuItemEntity value) {
//通过SQLiteStatement的bind方法,可以很巧妙的将类对象数据转化为数据库要操作的数据类型。
stmt.bindLong(1, (long)value.getId());//按顺序依次放入SQLiteStatement对象。
if(value.date == null) {
stmt.bindNull(2);
} else {
stmt.bindString(2, value.date);
}
//通过DB类注入的自界说转化器,我们可以将任何对象类型持久化到数据库中,并且很便捷的从数据库反序列化出来
String _tmp = DateConverter.toZhihuStoriesEntity(value.stories);
if(_tmp == null) {
stmt.bindNull(3);
} else {
stmt.bindString(3, _tmp);
}
String _tmp_1 = DateConverter.toZhihuStoriesEntity(value.top_stories);
if(_tmp_1 == null) {
stmt.bindNull(4);
} else {
stmt.bindString(4, _tmp_1);
}
}
};
}
public void insertItem(ZhihuItemEntity products) {
this.__db.beginTransaction();
try {
//借助SQLiteStatement类操作数据库,既优化了数据库操作性能,又巧妙的bind了对象类型数据。
this.__insertionAdapterOfZhihuItemEntity.insert(products);
this.__db.setTransactionSuccessful();
} finally {
//这里很重要,我们平时操作数据库或流必须要做 finally块 关闭资源。
this.__db.endTransaction();
}
}
实现类中可以看出insert是通过EntityInsertionAdapter类完成操作的,而EntityInsertionAdapter内部会持有个SupportSQLiteStatement,实在就是_SQLiteStatement_类的抽象封装。 实在例获取是通过RoomData内部方法compileStatement()得到的。
研究下RoomData抽象类源码:
public abstract class RoomDatabase {
// set by the generated open helper.
protected volatile SupportSQLiteDatabase mDatabase;//SQLiteDatabase类的封装抽象层
private SupportSQLiteOpenHelper mOpenHelper;//SQLiteOpenHelper类的封装抽象层
private final InvalidationTracker mInvalidationTracker;//绑定命据变更监听器,如在数据变化时通知LiveData
protected abstract SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config);
protected abstract InvalidationTracker createInvalidationTracker();
public Cursor query(String query, @Nullable Object[] args) {
return mOpenHelper.getWritableDatabase().query(new SimpleSQLiteQuery(query, args));
}
public Cursor query(SupportSQLiteQuery query) {
assertNotMainThread();//每次数据库操作查抄线程
return mOpenHelper.getWritableDatabase().query(query);
}
public SupportSQLiteStatement compileStatement(String sql) {
assertNotMainThread();
return mOpenHelper.getWritableDatabase().compileStatement(sql);
}
public void beginTransaction() {
assertNotMainThread();
mInvalidationTracker.syncTriggers();
mOpenHelper.getWritableDatabase().beginTransaction();
}
public void endTransaction() {
mOpenHelper.getWritableDatabase().endTransaction();
if (!inTransaction()) {
// enqueue refresh only if we are NOT in a transaction. Otherwise, wait for the last
// endTransaction call to do it.
mInvalidationTracker.refreshVersionsAsync();
}
}
public static class Builder {
private MigrationContainer mMigrationContainer;//数据库升级辅助类
@NonNull
public Builder addCallback(@NonNull Callback callback) {
if (mCallbacks == null) {
mCallbacks = new ArrayList<>();
}
mCallbacks.add(callback);
return this;
}
@NonNull
public T build() {
//noinspection ConstantConditions
if (mContext == null) {
throw new IllegalArgumentException(“Cannot provide null context for the database.”);
}
//noinspection ConstantConditions
if (mDatabaseClass == null) {
throw new IllegalArgumentException(“Must provide an abstract class that”


  • " extends RoomDatabase");
    }
    if (mFactory == null) {
    //默认的SupportSQLiteOpenHelper创建工厂
    mFactory = new FrameworkSQLiteOpenHelperFactory();//SupportSQLiteOpenHelper的实现类,通过mDelegate带来类操作真正的SQLiteOpenHelper
    }
    DatabaseConfiguration configuration =
    new DatabaseConfiguration(mContext, mName, mFactory, mMigrationContainer,
    mCallbacks, mAllowMainThreadQueries, mRequireMigration);
    //最终通过反射加载体系帮我们实现的真正RoomData
    T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
    db.init(configuration);
    return db;
    }
public abstract static class Callback {
public void onCreate(@NonNull SupportSQLiteDatabase db) {
}
public void onOpen(@NonNull SupportSQLiteDatabase db) {
}
}
}
DB是通过Build设计模式获取实例的,在build过程中,可以添加CallBack抽象类回调数据的_onCreate_和_onOpen_。 这里发现个问题,抽象层封装那么深,*onUpgrade()*方法怎么回调呢?数据库的升级怎么添加自己的逻辑呢?奥秘在MigrationContainer类。
public static class MigrationContainer {
private SparseArrayCompat<SparseArrayCompat> mMigrations =
new SparseArrayCompat<>();
public void addMigrations(Migration… migrations) {
for (Migration migration : migrations) {
addMigration(migration);
}
}
private void addMigration(Migration migration) {
final int start = migration.startVersion;
final int end = migration.endVersion;
SparseArrayCompat targetMap = mMigrations.get(start);
if (targetMap == null) {
targetMap = new SparseArrayCompat<>();
mMigrations.put(start, targetMap);
}
Migration existing = targetMap.get(end);
if (existing != null) {
Log.w(Room.LOG_TAG, "Overriding migration " + existing + " with " + migration);
}
targetMap.append(end, migration);
}
@SuppressWarnings(“WeakerAccess”)
@Nullable
public List findMigrationPath(int start, int end) {
if (start == end) {
return Collections.emptyList();
}
boolean migrateUp = end > start;
List result = new ArrayList<>();
return findUpMigrationPath(result, migrateUp, start, end);
}
}
public abstract class Migration {
public final int startVersion;
public final int endVersion;
public Migration(int startVersion, int endVersion) {
this.startVersion = startVersion;
this.endVersion = endVersion;
}
public abstract void migrate(@NonNull SupportSQLiteDatabase database);
}
}
在Room.databaseBuilder过程中,可以通过*addMigration()*方法,设置多个或一个Migration。
在RoomOpenHelper的onUpgrade()方法中会依次调用升级范围内的Migration:
@Override
public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
boolean migrated = false;
if (mConfiguration != null) {
List migrations = mConfiguration.migrationContainer.findMigrationPath(
oldVersion, newVersion);
if (migrations != null) {
for (Migration migration : migrations) {
migration.migrate(db);
}
}
}
}
分析Room到这里基本原理已相识,并且我们可以封装自己的Callback接口,对业务模块依次分发onCreate、onUpgrade方法,同一管理数据库的创建和升级。
Retrofit

当前业界很流行,且很精良的开源网络库,基于OkHttp之前开发。
   A type-safe HTTP client for Android and Java
  个人理解Retrofit是高度抽象,且和业务耦合度很低的网络库,通过各种数据转化器或适配器,使得网络返回数据可以很奇妙的直接转化为我们想要的类型,与本地数据的缓存及持久化高度无缝对接,大大减少了开发投入。并且使得项目研发更易模块化和迭代升级。
基本用法可以移步官网学习研究,这里只分析下怎样构造自界说返回类型,默认通用的哀求返回如下:
XXXService service = retrofit.create(XXXService.class);
Call<List> repos = service.listRepos(“xxx”);
public T create(final Class service) {
Utils.validateServiceInterface(service);
if (validateEagerly) {
eagerlyValidateMethods(service);
}
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
@Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
ServiceMethod<Object, Object> serviceMethod =
(ServiceMethod<Object, Object>) loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}
retrofit.create方法内部通过java动态代理,链接接口方法,替换转化范型类型及返回类型。 Retrofit.Builder有两个重要方法,影响着*service.listRepos()*方法的返回值类型及反序类型。它们分别是:
/** Add converter factory for serialization and deserialization of objects. */
//影响者Call接口中的范型类型
public Builder addConverterFactory(Converter.Factory factory) {
converterFactories.add(checkNotNull(factory, “factory == null”));
return this;
}
/**


  • Add a call adapter factory for supporting service method return types other than {@link
  • Call}.
  • 影响者Call接口的详细实现类型
    */
    public Builder addCallAdapterFactory(CallAdapter.Factory factory) {
    adapterFactories.add(checkNotNull(factory, “factory == null”));
    return this;
    }
通过addConverterFactory方法,可以将网络返回数据直接转化为本地的详细实体类型,并且retrofit已经为我们提供了常见协议数据类型的封装库,如下:
Converter依赖Gsoncom.squareup.retrofit2:converter-gson:xxxJacksoncom.squareup.retrofit2:converter-jackson:xxxMoshicom.squareup.retrofit2:converter-moshi:xxxProtobufcom.squareup.retrofit2:converter-protobuf:xxxWirecom.squareup.retrofit2:converter-wire:xxxSimple XMLcom.squareup.retrofit2:converter-simplexml:xxxScalarscom.squareup.retrofit2:converter-scalars:xxx Builder每添加一个转化器会保存在*List<Converter.Factory>*类型列表中去。通过以下代码转化为目标类型。
for (int i = start, count = converterFactories.size(); i < count; i++) {
Converter.Factory factory = converterFactories.get(i);
Converter<?, RequestBody> converter =
factory.requestBodyConverter(type, parameterAnnotations, methodAnnotations, this);
if (converter != null) {
//noinspection unchecked
return (Converter<T, RequestBody>) converter;
}
}
当然也可以自界说Converter类型:
public interface Converter<F, T> {
T convert(F value) throws IOException;
abstract class Factory {
// 这里创建从ResponseBody别的类型的Converter,如果不能处理惩罚返回null
// 主要用于对响应体的处理惩罚
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
Retrofit retrofit) {
return null;
}
// 在这里创建 从自定类型到ResponseBody 的Converter,不能处理惩罚就返回null,
public Converter<?, RequestBody> requestBodyConverter(Type type,
Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
//在这里实现详细转化逻辑
}
// Retrfofit对于上面的几个注解默认使用的是调用toString方法
public Converter<?, String> stringConverter(Type type, Annotation[] annotations,
Retrofit retrofit) {
//在这里实现详细转化逻辑
}
}
}
Retrofit通过_addCallAdapterFactory_方法可以支持返回类型_Java8_或_rxjava_的处理惩罚(也需要添加gradle依赖库)。
new Retrofit.Builder()
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
三. 封装、整合各框架到项目中去

主要是用LiveData将各框架的数据获取及页面更新,按照MVVM头脑整合起来, 使得项目结构符合官方给出的架构图建议,搭建一层逻辑结构,使得更加方便的使用各个组件库。

从上到下的逻辑顺序,依次构建各个业务层 需要的逻辑控件:
1.编写需要数据初始化或更新UI的接口方法,并在Observer中更新。

viewModel.getEssayData().observe(this, new Observer<Resource>() {
@Override
public void onChanged(@Nullable Resource essayDayEntityResource) {
//数据源内数据变动后自动回调该接口,然后更新到UI上
updateUI(essayDayEntityResource.data);
}
});
2.构建UI层需要的ViewModel

public class EssayViewModel extends AndroidViewModel {
private EssayRepository mRepository;
private MediatorLiveData<Resource> mCache;
public EssayViewModel(Application app) {
super(app);
mRepository = new EssayRepository(app);
}
public LiveData<Resource> getEssayData() {
if (mCache == null) {
//初始化后,从缓存读取
mCache = mRepository.loadEssayData();
}
return mCache;
}
public void updateCache() {
final LiveData<Resource> update = mRepository.update();
mCache.addSource(update, new Observer<Resource>() {
@Override
public void onChanged(@Nullable Resource zhihuItemEntityResource) {
mCache.setValue(zhihuItemEntityResource);
}
});
}
public void addMore(){
//TODO: 加载更多
}
}
3.实现Repository类,管理数据获取渠道。

这里按照官方知道,写了个抽象的数据源类,每次先从本地DB取数据,然后获取网络数据更新到数据库,通过LiveData更新到UI层。
public abstract class AbsDataSource<ResultType, RequestType> {
private final MediatorLiveData<Resource> result = new MediatorLiveData<>();
@WorkerThread
protected abstract void saveCallResult(@NonNull RequestType item);
@MainThread
protected abstract boolean shouldFetch(@Nullable ResultType data);
// Called to get the cached getDate from the database
@NonNull
@MainThread
protected abstract LiveData loadFromDb();
@NonNull
@MainThread
protected abstract LiveData<IRequestApi> createCall();
@MainThread
protected abstract void onFetchFailed();
@MainThread
public AbsDataSource() {
final LiveData dbSource = loadFromDb();
result.setValue(Resource.loading(dbSource.getValue()));
result.addSource(dbSource, new Observer() {
@Override
public void onChanged(@Nullable ResultType resultType) {
result.removeSource(dbSource);
if (shouldFetch(resultType)) {
fetchFromNetwork(dbSource);
} else {
result.addSource(dbSource, new Observer() {
@Override
public void onChanged(@Nullable ResultType resultType) {
result.setValue(Resource.success(resultType));
}
});
}
}
});
}
private void fetchFromNetwork(final LiveData dbSource) {
final LiveData<IRequestApi> apiResponse = createCall();
result.addSource(dbSource, new Observer() {
@Override
public void onChanged(@Nullable ResultType resultType) {
result.setValue(Resource.loading(resultType));
}
});
result.addSource(apiResponse, new Observer<IRequestApi>() {
@Override
public void onChanged(@Nullable final IRequestApi requestTypeRequestApi) {
result.removeSource(apiResponse);
result.removeSource(dbSource);
//noinspection ConstantConditions
if (requestTypeRequestApi.isSuccessful()) {
saveResultAndReInit(requestTypeRequestApi);
} else {
onFetchFailed();
result.addSource(dbSource, new Observer() {
@Override
public void onChanged(@Nullable ResultType resultType) {
result.setValue(
Resource.error(requestTypeRequestApi.getErrorMsg(), resultType));
}
});
}
}
});
}
@MainThread
private void saveResultAndReInit(final IRequestApi response) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void… voids) {
saveCallResult(response.getBody());
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
// we specially request a new live getDate,
// otherwise we will get immediately last cached value,
// which may not be updated with latest results received from network.
result.addSource(loadFromDb(), new Observer() {
@Override
public void onChanged(@Nullable ResultType resultType) {
result.setValue(Resource.success(resultType));
}
});
}
}.execute();
}
public final MediatorLiveData<Resource> getAsLiveData() {
return result;
}
}
4.封装Room数据库使用辅助类

这里二次封装了数据库回调接口,便于多个逻辑模块多数据库的同一管理使用。
public abstract class AbsDbCallback {
public abstract void create(SupportSQLiteDatabase db);
public abstract void open();
public abstract void upgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion);
}
public class DbCallbackHelper {
private static ArrayList mDbCallbacks = new ArrayList<>();
public static void init() {
mDbCallbacks.add(new EssayDbCallback());
总结:

面试是一个不停学习、不停自我提拔的过程,有机会还是出去面面,至少能想到查漏补缺效果,而且有些知识点,可能你自以为知道,但让你说,并不肯定能说得很好。
   有些东西有压力才有动力,而学到的知识点,都是钱(由于技能人员大部分情况是根据你的能力来定级、来发薪水的),技多不压身。
  附上我的面试各大专题整理: 面试指南,满满的都是干货,希望对大家有资助!

《Android学习条记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
一管理使用。
public abstract class AbsDbCallback {
public abstract void create(SupportSQLiteDatabase db);
public abstract void open();
public abstract void upgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion);
}
public class DbCallbackHelper {
private static ArrayList mDbCallbacks = new ArrayList<>();
public static void init() {
mDbCallbacks.add(new EssayDbCallback());
总结:

面试是一个不停学习、不停自我提拔的过程,有机会还是出去面面,至少能想到查漏补缺效果,而且有些知识点,可能你自以为知道,但让你说,并不肯定能说得很好。
   有些东西有压力才有动力,而学到的知识点,都是钱(由于技能人员大部分情况是根据你的能力来定级、来发薪水的),技多不压身。
  附上我的面试各大专题整理: 面试指南,满满的都是干货,希望对大家有资助!
[外链图片转存中…(img-nrWPX6IA-1715160558995)]
《Android学习条记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

石小疯

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表