注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

女儿拿着小天才电话手表问我App启动流程(下)

接 女儿拿着小天才电话手表问我App启动流程(上) 第四关:ActivityThread闪亮登场刚才说到由Zygote进行fork进程,并返回新进程的pid。其实这过程中也实例化ActivityThread对象。一起看看是怎么实现的: //RuntimeIni...
继续阅读 »

女儿拿着小天才电话手表问我App启动流程(上)


第四关:ActivityThread闪亮登场

刚才说到由Zygote进行fork进程,并返回新进程的pid。其实这过程中也实例化ActivityThread对象。一起看看是怎么实现的:


//RuntimeInit.java
protected static Runnable findStaticMain(String className, String[] argv,
ClassLoader classLoader) {
Class<?> cl;

try {
cl = Class.forName(className, true, classLoader);
} catch (ClassNotFoundException ex) {
throw new RuntimeException(
"Missing class when invoking static main " + className,
ex);
}

Method m;
try {
m = cl.getMethod("main", new Class[] { String[].class });
} catch (NoSuchMethodException ex) {
throw new RuntimeException(
"Missing static main on " + className, ex);
} catch (SecurityException ex) {
throw new RuntimeException(
"Problem getting static main on " + className, ex);
}
//...
return new MethodAndArgsCaller(m, argv);
}

原来是反射!通过反射调用了ActivityThread 的 main 方法。ActivityThread大家应该都很熟悉了,代表了Android的主线程,而main方法也是app的主入口。这不对上了!新建进程的时候就调用了,可不是主入口嘛。来看看这个主入口。


public static void main(String[] args) {
//...
Looper.prepareMainLooper();

ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);

//...

if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
//...
Looper.loop();

throw new RuntimeException("Main thread loop unexpectedly exited");
}

main方法主要创建了ActivityThread,创建了主线程的Looper对象,并开始loop循环。除了这些,还要告诉AMS,我醒啦,进程创建好了!也就是上述代码中的attach方法,最后会转到AMSattachApplicationLocked方法,一起看看这个方法干了啥:


//ActivitymanagerService.java
private final boolean attachApplicationLocked(IApplicationThread thread,
int pid, int callingUid, long startSeq) {
//...
ProcessRecord app;
//...
thread.bindApplication(processName, appInfo, providers, null, profilerInfo,
null, null, null, testMode,
mBinderTransactionTrackingEnabled, enableTrackAllocation,
isRestrictedBackupMode || !normalMode, app.isPersistent(),
new Configuration(app.getWindowProcessController().getConfiguration()),
app.compat, getCommonServicesLocked(app.isolated),
mCoreSettingsObserver.getCoreSettingsLocked(),
buildSerial, autofillOptions, contentCaptureOptions);
//...
app.makeActive(thread, mProcessStats);

//...
// See if the top visible activity is waiting to run in this process...
if (normalMode) {
try {
didSomething = mAtmInternal.attachApplication(app.getWindowProcessController());
} catch (Exception e) {
Slog.wtf(TAG, "Exception thrown launching activities in " + app, e);
badApp = true;
}
}
//...
}

//ProcessRecord.java
public void makeActive(IApplicationThread _thread, ProcessStatsService tracker) {
//...
thread = _thread;
mWindowProcessController.setThread(thread);
}

这里主要做了三件事:



  • bindApplication方法,主要用来启动Application。
  • makeActive方法,设定WindowProcessController里面的线程,也就是上文中说过判断进程是否存在所用到的。
  • attachApplication方法,启动根Activity。

第五关:创建Application

接着上面看,按照我们所熟知的,应用启动后,应该就是启动Applicaiton,启动Activity。看看是不是怎么回事:


    //ActivityThread#ApplicationThread
public final void bindApplication(String processName, ApplicationInfo appInfo,
List<ProviderInfo> providers, ComponentName instrumentationName,
ProfilerInfo profilerInfo, Bundle instrumentationArgs,
IInstrumentationWatcher instrumentationWatcher,
IUiAutomationConnection instrumentationUiConnection, int debugMode,
boolean enableBinderTracking, boolean trackAllocation,
boolean isRestrictedBackupMode, boolean persistent, Configuration config,
CompatibilityInfo compatInfo, Map services, Bundle coreSettings,
String buildSerial, AutofillOptions autofillOptions,
ContentCaptureOptions contentCaptureOptions) {
AppBindData data = new AppBindData();
data.processName = processName;
data.appInfo = appInfo;
data.providers = providers;
data.instrumentationName = instrumentationName;
data.instrumentationArgs = instrumentationArgs;
data.instrumentationWatcher = instrumentationWatcher;
data.instrumentationUiAutomationConnection = instrumentationUiConnection;
data.debugMode = debugMode;
data.enableBinderTracking = enableBinderTracking;
data.trackAllocation = trackAllocation;
data.restrictedBackupMode = isRestrictedBackupMode;
data.persistent = persistent;
data.config = config;
data.compatInfo = compatInfo;
data.initProfilerInfo = profilerInfo;
data.buildSerial = buildSerial;
data.autofillOptions = autofillOptions;
data.contentCaptureOptions = contentCaptureOptions;
sendMessage(H.BIND_APPLICATION, data);
}

public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case BIND_APPLICATION:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");
AppBindData data = (AppBindData)msg.obj;
handleBindApplication(data);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
}
}

复制代码

可以看到这里有个H,H是主线程的一个Handler类,用于处理需要主线程处理的各类消息,包括BIND_SERVICE,LOW_MEMORY,DUMP_HEAP等等。接着看handleBindApplication:


private void handleBindApplication(AppBindData data) {
//...
try {
final ClassLoader cl = instrContext.getClassLoader();
mInstrumentation = (Instrumentation)
cl.loadClass(data.instrumentationName.getClassName()).newInstance();
}
//...
Application app;
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites();
final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy();
try {
// If the app is being launched for full backup or restore, bring it up in
// a restricted environment with the base application class.
app = data.info.makeApplication(data.restrictedBackupMode, null);
mInitialApplication = app;
// don't bring up providers in restricted mode; they may depend on the
// app's custom Application class
if (!data.restrictedBackupMode) {
if (!ArrayUtils.isEmpty(data.providers)) {
installContentProviders(app, data.providers);
}
}

// Do this after providers, since instrumentation tests generally start their
// test thread at this point, and we don't want that racing.
try {
mInstrumentation.onCreate(data.instrumentationArgs);
}
//...
try {
mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
if (!mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
}
}
}
//...
}

这里信息量就多了,一点点的看:



  • 首先,创建了Instrumentation,也就是上文一开始startActivity的第一步。每个应用程序都有一个Instrumentation,用于管理这个进程,比如要创建Activity的时候,首先就会执行到这个类里面。
  • makeApplication方法,创建了Application,终于到这一步了。最终会走到newApplication方法,执行Application的attach方法。

public Application newApplication(ClassLoader cl, String className, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
Application app = getFactory(context.getPackageName())
.instantiateApplication(cl, className);
app.attach(context);
return app;
}

attach方法有了,onCreate方法又是何时调用的呢?马上来了:


instrumentation.callApplicationOnCreate(app);

public void callApplicationOnCreate(Application app) {
app.onCreate();
}

也就是创建Application->attach->onCreate调用顺序。


等等,在onCreate之前还有一句重要的代码:


installContentProviders

这里就是启动Provider的相关代码了,具体逻辑就不分析了。


第六关:启动Activity

说完bindApplication,该说说后续了,上文第五关说到,bindApplication方法之后执行的是attachApplication方法,最终会执行到ActivityThread的handleLaunchActivity方法:


public Activity handleLaunchActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions, Intent customIntent) {
//...
WindowManagerGlobal.initialize();
//...
final Activity a = performLaunchActivity(r, customIntent);
//...
return a;
}

首先,初始化了WindowManagerGlobal,这是个啥呢? 没错,就是WindowManagerService了,也为后续窗口显示等作了准备。


继续看performLaunchActivity:


private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
//创建ContextImpl
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
//创建Activity
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
}

try {
if (activity != null) {
//完成activity的一些重要数据的初始化
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback,
r.assistToken);

if (customIntent != null) {
activity.mIntent = customIntent;
}

//设置activity的主题
int theme = r.activityInfo.getThemeResource();
if (theme != 0) {
activity.setTheme(theme);
}

//调用activity的onCreate方法
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
}
}

return activity;
}

哇,终于看到onCreate方法了。稳住,还是一步步看看这段代码。


首先,创建了ContextImpl对象,ContextImpl可能有的朋友不知道是啥,ContextImpl继承自Context,其实就是我们平时用的上下文。有的同学可能表示,这不对啊,获取上下文明明获取的是Context对象。来一起跟随源码看看。


//Activity.java
Context mBase;

@Override
public Executor getMainExecutor() {
return mBase.getMainExecutor();
}

@Override
public Context getApplicationContext() {
return mBase.getApplicationContext();
}

这里可以看到,我们平时用的上下文就是这个mBase,那么找到这个mBase是啥就行了:


protected void attachBaseContext(Context base) {
if (mBase != null) {
throw new IllegalStateException("Base context already set");
}
mBase = base;
}

//一层层往上找

final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor) {

attachBaseContext(context);

mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}


}

这不就是,,,刚才一开始performLaunchActivity方法里面的attach吗?太巧了,所以这个ContextImpl就是我们平时所用的上下文。


顺便看看attach还干了啥?新建了PhoneWindow,建立自己和Window的关联,并设置了setSoftInputMode等等。


ContextImpl创建完之后,会通过类加载器创建Activity的对象,然后设置好activity的主题,最后调用了activity的onCreate方法。


总结

再一起捋一遍App的启动流程:



  • Launcher被调用点击事件,转到Instrumentation类的startActivity方法。
  • Instrumentation通过跨进程通信告诉AMS要启动应用的需求。
  • AMS反馈Launcher,让Launcher进入Paused状态
  • Launcher进入Paused状态,AMS转到ZygoteProcess类,并通过socket与Zygote通信,告知Zygote需要新建进程。
  • Zygote fork进程,并调用ActivityThread的main方法,也就是app的入口。
  • ActivityThread的main方法新建了ActivityThread实例,并新建了Looper实例,开始loop循环。
  • 同时ActivityThread也告知AMS,进程创建完毕,开始创建Application,Provider,并调用Applicaiton的attach,onCreate方法。
  • 最后就是创建上下文,通过类加载器加载Activity,调用Activity的onCreate方法。

至此,应用启动完毕。


当然,分析源码的目的一直都不是为了学知识而学,而是理解了这些基础,我们才能更好的解决问题。 学习了App的启动流程,我们可以再思考下一些之前没理解透的问题,比如启动优化


分析启动过程,其实可以优化启动速度的地方有三个地方:



  • Application的attach方法,MultiDexApplication会在方法里面会去执行MultiDex逻辑。所以这里可以进行MultiDex优化,比如今日头条方案就是单独启动一个进程的activity去加载MultiDex。
  • Application的onCreate方法,大量三方库的初始化都在这里进行,所以我们可以开启线程池,懒加载等等。把每个启动任务进行区分,哪些可以子线程运行,哪些有先后顺序。
  • Activity的onCreate方法,同样进行线程处理,懒加载。或者预创建Activity,提前类加载等等。

最后希望各位老铁都能有一个乖巧可爱漂亮的女儿/儿子。😊


附件

fork使用多线程
今日头条启动优化
app启动流程分析


作者:积木zz
来源:https://juejin.cn/post/6867744083809419277

收起阅读 »

女儿拿着小天才电话手表问我App启动流程(上)

首先,new一个女儿,var mDdaughter = new 女儿("6岁",“漂亮可爱”,“健康乖巧”,“最喜欢玩小天才电话手表和她的爸爸”)“爸爸爸爸,你说我玩的这个小天才电话手表怎么这么厉害,随便点一下这个小图片,这个应用就冒出来了,就可以听儿歌了。好...
继续阅读 »



前言

首先,new一个女儿,

var mDdaughter = new 女儿("6岁",“漂亮可爱”,“健康乖巧”,“最喜欢玩小天才电话手表和她的爸爸”)

好了,女儿有了,有一天,女儿问我:

“爸爸爸爸,你说我玩的这个小天才电话手表怎么这么厉害,随便点一下这个小图片,这个应用就冒出来了,就可以听儿歌了。好神奇啊。”

我心里一惊: img

小天才电话手表的系统就是Android,所以这不就是。。面试官常考的应用启动流程嘛!
女儿也要来面试我了吗!😭
好了,既然女儿问了,那就答吧。
但是,对付这个小小的0经验面试官,我该咋说呢?

解答小小面试官

女儿,你可以把手表里面想象成一个幼儿园,里面有一个老师,一个班长,一个班干部,以及一大堆小朋友。

  • 一个老师:Z老师(Zygote进程)

  • 一个班长:小A(ActivityManagerService)

  • 一个班干部:小L(Launcher桌面应用)

  • 一大堆小朋友:所有应用,包括音乐小朋友,聊天小朋友,日历小朋友等等。

img

应用启动过程就像一个小朋友被叫醒一样,开机之后呢,Z老师会依次叫醒班长和班干部(SystemServer#ActivityManagerService,Launcher),小L醒了之后就会去了解手表里有哪些小朋友,长什么样(icon,name),家庭信息(包名,androidmanifest)等等,然后一个个把小朋友的照片(icon)贴到自己的身上。比如有音乐小朋友,聊天小朋友,日历小朋友,其实也就是你手表上这个桌面啦。

这时候你要点开一个音乐小朋友呢(startActivity),小L就会通知班长小A(Binder),小A知道了之后,让小L自己休息下(Paused),然后就去找Z老师了。Z老师就负责叫音乐小朋友起床了(fork进程,启动ActivityThread),音乐小朋友起来后就又找小A带她去洗脸刷牙(启动ApplicationThread,Activity),都弄完了就可以进行各种表演了,唱歌啊,跳舞啊。

不是很明白啊?我们一起聊个天你就懂了,假如我是Launcher

img

img

女儿似懂非懂的给我点了一个赞👍,爸爸你真棒。

十五年后

mDdaughter.grow(15)
mDdaughter.study("Android")

过了十五年,女儿已经21岁了,正在学习Android,考虑要不要女从父业。

这天,她一脸疑惑的来找我: “爸,这个app启动到底是怎么个流程啊,我看了好久还是不大明白,要不你再跟我详细讲一遍吧?” “好嘞,别担心,我这次详细跟你说说”

解答Android程序媛

还记得我小时候跟你说过的故事吗,Android系统就像一个幼儿园,有一个大朋友叫Launcher,身上会贴很多其他小朋友的名片。这个Launcher就是我们的桌面了,它通过PackageManagerService获知了系统里所有应用的信息,并展示了出来,当然它本身也是一个应用。

通过点击一个应用图标,也就是触发了点击事件,最后会执行到startActivity方法。这里也就和启动Activity步骤重合上了。

那么这个startActivity干了啥?是怎么通过重重关卡唤醒这个应用的?

首先,介绍下系统中那些重要的成员,他们在app启动流程中都担任了重要的角色.

系统成员介绍

  • init进程,Android系统启动后,Zygote并不是第一个进程,而是linux的根进程init进程,然后init进程才会启动Zygote进程。

  • Zygote进程,所有android进程的父进程,当然也包括SystemServer进程

  • SystemServer进程,正如名字一样,系统服务进程,负责系统中大大小小的事物,为此也是启动了三员大将(ActivityManagerService,PackageManagerService,WindowManagerService)以及binder线程池。

  • ActivityManagerService,主要负责系统中四大组件的启动、切换、调度及应用进程的管理和调度等工作,对于一些进程的启动,都会通过Binder通信机制传递给AMS,再处理给Zygote。

  • PackageManagerService,主要负责应用包的一些操作,比如安装,卸载,解析AndroidManifest.xml,扫描文件信息等等。

  • WindowManagerService,主要负责窗口相关的一些服务,比如窗口的启动,添加,删除等。

  • Launcher,桌面应用,也是属于应用,也有自己的Activity,一开机就会默认启动,通过设置Intent.CATEGORY_HOME的Category隐式启动。

搞清楚这些成员,就跟随我一起看看怎么过五关斩六将,最终启动了一个App。

第一关:跨进程通信,告诉系统我的需求

首先,要告诉系统,我Launcher要启动一个应用了,调用Activity.startActivityForResult方法,最终会转到mInstrumentation.execStartActivity方法。 由于Launcher自己处在一个单独的进程,所以它需要跨进程告诉系统服务我要启动App的需求。 找到要通知的Service,名叫ActivityTaskManagerService,然后使用AIDL,通过Binder与他进行通信。

这里的简单说下ActivityTaskManagerService(简称ATMS)。原来这些通信工作都是属于ActivityManagerService,现在分了一部分工作给到ATMS,主要包括四大组件的调度工作。也是由SystemServer进程直接启动的,相关源码可见ActivityManagerService.Lifecycle.startService方法,感兴趣朋友可以自己看看。

接着说跨进程通信,相关代码如下:

//Instrumentation.java
int result = ActivityTaskManager.getService()
  .startActivity(whoThread, who.getBasePackageName(), intent,
                  intent.resolveTypeIfNeeded(who.getContentResolver()),
                  token, target != null ? target.mEmbeddedID : null,
                  requestCode, 0, null, options);


//ActivityTaskManager.java            
public static IActivityTaskManager getService() {
   return IActivityTaskManagerSingleton.get();
}
private static final Singleton<IActivityTaskManager> IActivityTaskManagerSingleton =
   new Singleton<IActivityTaskManager>() {
   @Override
   protected IActivityTaskManager create() {
       final IBinder b = ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE);
       return IActivityTaskManager.Stub.asInterface(b);
  }
};

//ActivityTaskManagerService.java
public class ActivityTaskManagerService extends IActivityTaskManager.Stub

   public static final class Lifecycle extends SystemService {
       private final ActivityTaskManagerService mService;

       public Lifecycle(Context context) {
           super(context);
           mService = new ActivityTaskManagerService(context);
      }

       @Override
       public void onStart() {
           publishBinderService(Context.ACTIVITY_TASK_SERVICE, mService);
           mService.start();
      }
  }

startActivity我们都很熟悉,平时启动Activity都会使用,启动应用也是从这个方法开始的,也会同样带上intent信息,表示要启动的是哪个Activity。

另外要注意的一点是,startActivity之后有个checkStartActivityResult方法,这个方法是用作检查启动Activity的结果。当启动Activity失败的时候,就会通过这个方法抛出异常,比如有我们常见的问题:未在AndroidManifest.xml注册。

public static void checkStartActivityResult(int res, Object intent) {
   switch (res) {
       case ActivityManager.START_INTENT_NOT_RESOLVED:
       case ActivityManager.START_CLASS_NOT_FOUND:
           if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
               throw new ActivityNotFoundException(
               "Unable to find explicit activity class "
               + ((Intent)intent).getComponent().toShortString()
               + "; have you declared this activity in your AndroidManifest.xml?");
           throw new ActivityNotFoundException(
               "No Activity found to handle " + intent);
       case ActivityManager.START_PERMISSION_DENIED:
           throw new SecurityException("Not allowed to start activity "
                                       + intent);
       case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
           throw new AndroidRuntimeException(
               "FORWARD_RESULT_FLAG used while also requesting a result");
       case ActivityManager.START_NOT_ACTIVITY:
           throw new IllegalArgumentException(
               "PendingIntent is not an activity");
           //...
  }
}

第二关:通知Launcher可以休息了

ATMS收到要启动的消息后,就会通知上一个应用,也就是Launcher可以休息会了,进入Paused状态。

//ActivityStack.java

private boolean resumeTopActivityInnerLocked(ActivityRecord prev, ActivityOptions options) {
   //...
   ActivityRecord next = topRunningActivityLocked(true /* focusableOnly */);
   //...
   boolean pausing = getDisplay().pauseBackStacks(userLeaving, next, false);
   if (mResumedActivity != null) {
       if (DEBUG_STATES) Slog.d(TAG_STATES,
                                "resumeTopActivityLocked: Pausing " + mResumedActivity);
       pausing |= startPausingLocked(userLeaving, false, next, false);
  }
   //...

   if (next.attachedToProcess()) {
       //应用已经启动
       try {
           //...
           transaction.setLifecycleStateRequest(
               ResumeActivityItem.obtain(next.app.getReportedProcState(),
                                         getDisplay().mDisplayContent.isNextTransitionForward()));
           mService.getLifecycleManager().scheduleTransaction(transaction);
           //...
      } catch (Exception e) {
           //...
           mStackSupervisor.startSpecificActivityLocked(next, true, false);
           return true;
      }
       //...
       // From this point on, if something goes wrong there is no way
       // to recover the activity.
       try {
           next.completeResumeLocked();
      } catch (Exception e) {
           // If any exception gets thrown, toss away this
           // activity and try the next one.
           Slog.w(TAG, "Exception thrown during resume of " + next, e);
           requestFinishActivityLocked(next.appToken, Activity.RESULT_CANCELED, null,
                                       "resume-exception", true);
           return true;
      }
  } else {
       //冷启动流程
       mStackSupervisor.startSpecificActivityLocked(next, true, true);
  }        
}

这里有两个类没有见过:

  • ActivityStack,是Activity的栈管理,相当于我们平时项目里面自己写的Activity管理类,用于管理Activity的状态啊,如栈出栈顺序等等。

  • ActivityRecord,代表具体的某一个Activity,存放了该Activity的各种信息。

startPausingLocked方法就是让上一个应用,这里也就是Launcher进入Paused状态。 然后就会判断应用是否启动,如果已经启动了,就会走ResumeActivityItem的方法,看这个名字,结合应用已经启动的前提,是不是已经猜到了它是干吗的?没错,这个就是用来控制Activity的onResume生命周期方法的,不仅是onResume还有onStart方法,具体可见ActivityThread的handleResumeActivity方法源码。

如果应用没启动就会接着走到startSpecificActivityLocked方法,接着看。

第三关:是否已启动进程,否则创建进程

Launcher进入Paused之后,ActivityTaskManagerService就会判断要打开的这个应用进程是否已经启动,如果已经启动,则直接启动Activity即可,这也就是应用内的启动Activity流程。如果进程没有启动,则需要创建进程。

这里有两个问题:

  • 怎么判断应用进程是否存在呢?如果一个应用已经启动了,会在ATMS里面保存一个WindowProcessController信息,这个信息包括processName和uid,uid则是应用程序的id,可以通过applicationInfo.uid获取。processName则是进程名,一般为程序包名。所以判断是否存在应用进程,则是根据processName和uid去判断是否有对应的WindowProcessController,并且WindowProcessController里面的线程不为空。代码如下:

//ActivityStackSupervisor.java
void startSpecificActivityLocked(ActivityRecord r, boolean andResume, boolean checkConfig) {
   // Is this activity's application already running?
   final WindowProcessController wpc =
       mService.getProcessController(r.processName, r.info.applicationInfo.uid);

   boolean knownToBeDead = false;
   if (wpc != null && wpc.hasThread()) {
       //应用进程存在
       try {
           realStartActivityLocked(r, wpc, andResume, checkConfig);
           return;
      }
  }
}

//WindowProcessController.java
IApplicationThread getThread() {
   return mThread;
}

boolean hasThread() {
   return mThread != null;
}
  • 还有个问题就是怎么创建进程?还记得Z老师吗?对,就是Zygote进程。之前说了他是所有进程的父进程,所以就要通知Zygote去fork一个新的进程,服务于这个应用。

//ZygoteProcess.java
private Process.ProcessStartResult attemptUsapSendArgsAndGetResult(
   ZygoteState zygoteState, String msgStr)
   throws ZygoteStartFailedEx, IOException {
   try (LocalSocket usapSessionSocket = zygoteState.getUsapSessionSocket()) {
       final BufferedWriter usapWriter =
           new BufferedWriter(
           new OutputStreamWriter(usapSessionSocket.getOutputStream()),
           Zygote.SOCKET_BUFFER_SIZE);
       final DataInputStream usapReader =
           new DataInputStream(usapSessionSocket.getInputStream());

       usapWriter.write(msgStr);
       usapWriter.flush();

       Process.ProcessStartResult result = new Process.ProcessStartResult();
       result.pid = usapReader.readInt();
       // USAPs can't be used to spawn processes that need wrappers.
       result.usingWrapper = false;

       if (result.pid >= 0) {
           return result;
      } else {
           throw new ZygoteStartFailedEx("USAP specialization failed");
      }
  }
}

可以看到,这里其实是通过socket和Zygote进行通信,BufferedWriter用于读取和接收消息。这里将要新建进程的消息传递给Zygote,由Zygote进行fork进程,并返回新进程的pid。

可能又会有人问了?fork是啥?为啥这里又变成socket进行IPC通信,而不是Bindler了?

  • 首先,fork()是一个方法,是类Unix操作系统上创建进程的主要方法。用于创建子进程(等同于当前进程的副本)。

  • 那为什么fork的时候不用Binder而用socket了呢?主要是因为fork不允许存在多线程,Binder通讯偏偏就是多线程。

问题总是在不断产生,总有好奇的朋友会接着问,为什么fork不允许存在多线程?

收起阅读 »

Android 手把手带你搭建一个组件化项目架构

🔥 一、组件化作为一个单工程撸到底的开发人员,想试着将项目进行组件化改造,说动就动。毕竟技术都是写出来的,看着文章感觉懂了,但是实际开发中还是能遇到各种各样的问题,开始搞起来。💥 1.1 为什么使用组件化一直使用单工程撸到底,项目越来越大导致出现了不少的问题:...
继续阅读 »



🔥 一、组件化

作为一个单工程撸到底的开发人员,想试着将项目进行组件化改造,说动就动。毕竟技术都是写出来的,看着文章感觉懂了,但是实际开发中还是能遇到各种各样的问题,开始搞起来。

💥 1.1 为什么使用组件化

一直使用单工程撸到底,项目越来越大导致出现了不少的问题:

  • 查找问题慢:定位问题,需要在多个代码混合的模块中寻找和跳转。

  • 开发维护成本增加:避免代码的改动影响其它业务的功能,导致开发和维护成本不断增加。

  • 编译时间长:项目工程越大,编译完整代码所花费的时间越长。

  • 开发效率低:多人协作开发时,开发风格不一,又很难将业务完全分割,大家互相影响,导致开发效率低下。

  • 代码复用性差:写过的代码很难抽离出来再次利用。

💥 1.2 模块化与组件化

🌀 1.2.1 模块

一个程序按照其功能做拆分,分成相互独立的模块,以便于每个模块只包含与其功能相关的内容,比如登录模块首页模块等等。

🌀 1.2.2 组件

组件指的是单一的功能组件,如登录组件视频组件支付组件 等,每个组件都可以以一个单独的 module 开发,并且可以单独抽出来作为 SDK 对外发布使用。可以说往往一个模块包含了一个或多个组件。

💥 1.3 组件化的优势

组件化基于可重用的目的,将应用拆分成多个独立组件,以减少耦合:

  • 加快编译速度:每个业务功能都是一个单独的工程,可独立编译运行,拆分后代码量较少,编译自然变快。

  • 解耦:通过关注点分离的形式,将App分离成多个模块,每个模块都是一个组件。

  • 提高开发效率:多人开发中,每个组件模块由单人负责,降低了开发之间沟通的成本,减少因代码风格不一而产生的相互影响。

  • 代码复用:类似我们引用的第三方库,可以将基础组件或功能组件剥离。在新项目微调或直接使用。

💥 1.4 组件化需要解决的问题

  • 组件分层:怎么将一个项目分成多个组件、组件间的依赖关系是怎么样的?

  • 组件单独运行和集成调试:组件是如何独立运行和集成调试的?

  • 组件间通信:主项目与组件、组件与组件之间如何通信就变成关键?

🔥 二、组件分层

组件依赖关系是上层依赖下层,修改频率是上层高于下层。先上一张图:

img

💥 2.1 基础组件

基础公共模块,最底层的库:

  • 封装公用的基础组件;

  • 网络访问框架、图片加载框架等主流的第三方库;

  • 各种第三方SDK。

💥 2.2 common组件(lib_common)

  • 支撑业务组件、功能组件的基础(BaseActivity/BaseFragment等基础能力;

  • 依赖基础组件层;

  • 业务组件、功能组件所需的基础能力只需要依赖common组件即可获得。

💥 2.3 功能组件

  • 依赖基础组件层;

  • 对一些公用的功能业务进行封装与实现;

  • 业务组件可以在library和application之间切换,但是最后打包时必须是library ;

💥 2.4 业务组件

  • 可直接依赖基础组件层;同时也能依赖公用的一些功能组件;

  • 各组件之间不存在依赖关系,通过路由进行通信;

  • 业务组件可以在library和application之间切换,但是最后打包时必须是library ;

💥 2.5 主工程(app)

  • 只依赖各业务组件;

  • 除了一些全局的配置和主Activity之外,不包含任何业务代码,是应用的入口;

💥 2.6 完成后项目

img

这只是个大概,并不是说必须这样,可以按照自己的方式来。比如:你觉得基础组件比较多导致project里面的项目太多,那么你可以创建一个lib_base,然在lib_base里面再创建其他基础组件即可。

🔥 三、组件单独调试

💥 3.1 创建组件(收藏)

img

  • library和application之间切换:选择第一项。

  • 始终是library:选择第二项

这样尽可能的减少变动项,当然这仅仅是个建议,看个人习惯吧。

因为咱们创建的是一个module,所以在AndridManifest中添加android:exported="true"属性可直接构建一个APK。下面咱们看看如何生成不同的工程类型。

💥 3.2 动态配置组件的工程类型

在 AndroidStudio 开发 Android 项目时,使用的是 Gradle 来构建,具体来说使用的是 Android Gradle 插件来构建,Android Gradle 中提供了三种插件,在开发中可以通过配置不同的插件来构建不同的工程。

🌀 3.2.1 build.gradle(module)

//构建后输出一个 APK 安装包
apply plugin: 'com.android.application'
//构建后输出 ARR 包
apply plugin: 'com.android.library'
//配置一个 Android Test 工程
apply plugin: 'com.android.test'

独立调试:设置为 Application 插件。

集成调试:设置为 Library 插件。

🌀 3.2.2 设置gradle.properties

img

isDebug = true 独立调试

🌀 3.2.3 动态配制插件(build.gradle)

//注意gradle.properties中的数据类型都是String类型,使用其他数据类型需要自行转换
if(isDebug.toBoolean()){
   //构建后输出一个 APK 安装包
   apply plugin: 'com.android.application'
}else{
   //构建后输出 ARR 包
   apply plugin: 'com.android.library'
}

💥 3.3 动态配置组件的 ApplicationId 和 AndroidManifest 文件

  • 一个 APP 是只有一个 ApplicationId ,所以在单独调试集成调试组件的 ApplicationId 应该是不同的。

  • 单独调试时也是需要有一个启动页,当集成调试时主工程和组件的AndroidManifest文件合并会产生多个启动页。

根据上面动态配制插件的经验,我们也需要在build.gradle中动态配制ApplicationId 和 AndroidManifest 文件。

🌀 3.3.1 准备两个不同路径的 AndroidManifest 文件

img

有什么不同?咱们一起看看具体内容。

🌀 3.3.2 src/main/debug/AndroidManifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.scc.module.collect">

   <application
       android:allowBackup="true"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
       android:supportsRtl="true"
       android:theme="@style/Theme.SccMall">
       <activity android:name=".CollectActivity"
           android:exported="true">
           <intent-filter>
               <action android:name="android.intent.action.MAIN" />

               <category android:name="android.intent.category.LAUNCHER" />
           </intent-filter>
       </activity>
   </application>

</manifest>

🌀 3.3.3 src/main/debug/AndroidManifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.scc.module.collect">
   <application
       android:allowBackup="true"
       android:supportsRtl="true"
       >
       <activity android:name=".CollectActivity"/>
   </application>

</manifest>

🌀 3.3.4 动态配制(build.gradle)

defaultConfig {
   if(isDebug.toBoolean()){
       //独立调试的时候才能设置applicationId
       applicationId "com.scc.module.collect"
  }
}
sourceSets {
   main {
       if (isDebug.toBoolean()) {
           //独立调试
           manifest.srcFile 'src/main/debug/AndroidManifest.xml'
      } else {
           //集成调试
           manifest.srcFile 'src/main/AndroidManifest.xml'
      }
  }
}

💥 3.4 实现效果

🌀 3.4.1 独立调试

isDebug = true

img

🌀 3.4.2 集成调试

isDebug = false

img

🔥 四、Gradle配置统一管理

💥 4.1 config.gradle

当我们需要进行插件版本、依赖库版本升级时,项目多的话改起来很麻烦,这时就需要我们对Gradle配置统一管理。如下:

img

具体内容

ext{
   //组件独立调试开关, 每次更改值后要同步工程
   isDebug = true
   android = [
           // 编译 SDK 版本
           compileSdkVersion: 31,
           // 最低兼容 Android 版本
           minSdkVersion   : 21,
           // 最高兼容 Android 版本
           targetSdkVersion : 31,
           // 当前版本编号
           versionCode     : 1,
           // 当前版本信息
           versionName     : "1.0.0"
  ]
   applicationid = [
           app:"com.scc.sccmall",
           main:"com.scc.module.main",
           webview:"com.scc.module.webview",
           login:"com.scc.module.login",
           collect:"com.scc.module.collect"
  ]
   dependencies = [
           "appcompat"         :'androidx.appcompat:appcompat:1.2.0',
           "material"         :'com.google.android.material:material:1.3.0',
           "constraintlayout" :'androidx.constraintlayout:constraintlayout:2.0.1',
           "livedata"         :'androidx.lifecycle:lifecycle-livedata:2.4.0',
           "viewmodel"         :'androidx.lifecycle:lifecycle-viewmodel:2.4.0',
           "legacyv4"         :'androidx.legacy:legacy-support-v4:1.0.0',
           "splashscreen"     :'androidx.core:core-splashscreen:1.0.0-alpha01'
  ]
   libARouter= 'com.alibaba:arouter-api:1.5.2'
   libARouterCompiler = 'com.alibaba:arouter-compiler:1.5.2'
   libGson = 'com.google.code.gson:gson:2.8.9'
}

💥 4.2 添加配制文件build.gradle(project)

apply from:"config.gradle"

💥 4.3 其他组件使用

//build.gradle
//注意gradle.properties中的数据类型都是String类型,使用其他数据类型需要自行转换
if(isDebug.toBoolean()){
   //构建后输出一个 APK 安装包
   apply plugin: 'com.android.application'
}else{
   //构建后输出 ARR 包
   apply plugin: 'com.android.library'
}
android {
   compileSdkVersion 31

   defaultConfig {
       if(isDebug.toBoolean()){
           //独立调试的时候才能设置applicationId
           applicationId "com.scc.module.collect"
      }
       minSdkVersion 21
       targetSdkVersion 31
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
  }

   buildTypes {
       release {
           minifyEnabled false
           proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
      }
  }
   sourceSets {
       main {
           if (isDebug.toBoolean()) {
               //独立调试
               manifest.srcFile 'src/main/debug/AndroidManifest.xml'
          } else {
               //集成调试
               manifest.srcFile 'src/main/AndroidManifest.xml'
          }
      }
  }
   compileOptions {
       sourceCompatibility JavaVersion.VERSION_1_8
       targetCompatibility JavaVersion.VERSION_1_8
  }
}

dependencies {
//   implementation root.dependencies.appcompat
//   implementation root.dependencies.material
//   implementation root.dependencies.constraintlayout
//   implementation root.dependencies.livedata
//   implementation root.dependencies.viewmodel
//   implementation root.dependencies.legacyv4
//   implementation root.dependencies.splashscreen
//   implementation root.libARouter
   //上面内容在lib_common中已经添加咱们直接依赖lib_common
   implementation project(':lib_common')

   testImplementation 'junit:junit:4.+'
   androidTestImplementation 'androidx.test.ext:junit:1.1.2'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

🔥 五、组件间界面跳转(ARouter)

💥 5.1 介绍

Android 中的界面跳转那是相当简单,但是在组件化开发中,由于不同组件式没有相互依赖的,所以不可以直接访问彼此的类,这时候就没办法通过显式的方式实现了。

所以在这里咱们采取更加灵活的一种方式,使用 Alibaba 开源的 ARouter 来实现。

一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦

文档介绍的蛮详细的,感兴趣的可以自己实践一下。这里做个简单的使用。

💥 5.2 使用

🌀 5.2.1 添加依赖

先在统一的config.gradle添加版本等信息

ext{
  ...
   libARouter= 'com.alibaba:arouter-api:1.5.2'
   libARouterCompiler = 'com.alibaba:arouter-compiler:1.5.2'
}

因为所有的功能组件和业务组件都依赖lib_common,那么咱们先从lib_common开始配制

lib_common

dependencies {
   api root.libARouter
  ...
}

其他组件(如collect)

android {
   defaultConfig {
      ...
       javaCompileOptions {
           annotationProcessorOptions {
               arguments = [AROUTER_MODULE_NAME: project.getName()]
               //如果项目内有多个annotationProcessor,则修改为以下设置
               //arguments += [AROUTER_MODULE_NAME: project.getName()]
          }
      }
  }
}

dependencies {
   //arouter-compiler的注解依赖需要所有使用 ARouter 的 module 都添加依赖
   annotationProcessor root.libARouterCompiler
  ...
}

🌀 5.2.2 添加注解

你要跳转的Activity

// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = "/collect/CollectActivity")
public class CollectActivity extends AppCompatActivity {
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_collect);
  }
}

🌀 5.2.3 初始化SDK(主项目Application)

public class App extends BaseApplication {
   @Override
   public void onCreate() {
       super.onCreate();
       if (isDebug()) {           // 这两行必须写在init之前,否则这些配置在init过程中将无效
           ARouter.openLog();     // 打印日志
           ARouter.openDebug();   // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
      }
       ARouter.init(this); // 尽可能早,推荐在Application中初始化
  }
   private boolean isDebug() {
       return BuildConfig.DEBUG;
  }
}

💥 5.3 发起路由操作

🌀 5.3.1 应用内简单的跳转

ARouter.getInstance().build("/collect/CollectActivity").navigation();

这里是用module_main的HomeFragment跳转至module_collect的CollectActivity界面,两个module中不存在依赖关系。"/collect/CollectActivity"在上面已注册就不多描述了。

效果如下:

img

🌀 5.3.2 跳转并携带参数

这里是用module_main的MineFragment的Adapter跳转至module_webview的WebViewActivity界面,两个module中同样不存在依赖关系。

启动方

ARouter.getInstance().build("/webview/WebViewActivity")
  .withString("url", bean.getUrl())
  .withString("content",bean.getName())
  .navigation();

这里传了两个参数urlname到WebViewActivity,下面咱们看看WebViewActivity怎么接收。

接收方

//为每一个参数声明一个字段,并使用 @Autowired 标注
//URL中不能传递Parcelable类型数据,通过ARouter api可以传递Parcelable对象
//添加注解(必选)
@Route(path = "/webview/WebViewActivity")
public class WebViewActivity extends BaseActivity<ActivityWebviewBinding, WebViewViewModel> {
   //发送方和接收方定义的key名称相同则无需处理
   @Autowired
   public String url;
   //通过name来映射URL中的不同参数
   //发送方定义key为content,我们用title来接收
   @Autowired(name = "content")
   public String title;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       //注入参数和服务(这里用到@Autowired所以要设置)
       //不使用自动注入,可不写,如CollectActivity没接收参数就没有设置
       ARouter.getInstance().inject(this);
       binding.btnBoom.setText(String.format("%s,你来啦", title));
       //加载链接
       initWebView(binding.wbAbout, url);
  }
}

上效果图:

搞定,更多高级玩法可自行探索。

🌀 5.3.3 小记(ARouter目标不存在)

W/ARouter::: ARouter::There is no route match the path

这里出现个小问题,配置注释都好好的,但是发送发无论如何都找不到设置好的Activity。尝试方案:

  • Clean Project

  • Rebuild Project

  • 在下图也能找到ARouter内容。

后来修改Activity名称好了。

img

🔥 六、组件间通信(数据传递)

界面跳转搞定了,那么数据传递怎么办,我在module_main中使用悬浮窗,但是需要判断这个用户是否已登录,再执行后续逻辑,这个要怎么办?这里我们可以采用 接口 + ARouter 的方式来解决。

在这里可以添加一个 componentbase 模块,这个模块被所有的组件依赖

这里我们通过 module_main组件 中调用 module_login组件 中的方法来获取登录状态这个场景来演示。

💥 6.1 通过依赖注入解耦:服务管理(一) 暴露服务

🌀 6.1.1 创建 componentbase 模块(lib)

img

🌀 6.1.2 创建接口并继承IProvider

注意:接口必须继承IProvider,是为了使用ARouter的实现注入。

img

🌀 6.1.3 在module_login组件中实现接口

lib_common

所有业务组件和功能组件都依赖lib_common,所以咱们直接在lib_common添加依赖即可

dependencies {
  ...
   api project(":lib_componentbase")
}

module_login

dependencies {
  ...
   implementation project(':lib_common')
}

实现接口

//实现接口
@Route(path = "/login/AccountServiceImpl")
public class AccountServiceImpl implements IAccountService {
   @Override
   public boolean isLogin() {
       MLog.e("AccountServiceImpl.isLogin");
       return true;
  }

   @Override
   public String getAccountId() {
       MLog.e("AccountServiceImpl.getAccountId");
       return "1000";
  }

   @Override
   public void init(Context context) {

  }
}

img

💥 6.2 通过依赖注入解耦:服务管理(二) 发现服务

🌀 6.2.1 在module_main中调用调用是否已登入

public class HomeFragment extends BaseFragment<FragmentHomeBinding> {
   @Autowired
   IAccountService accountService;
   @Override
   public void onViewCreated(@NonNull @NotNull View view, @Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
       super.onViewCreated(view, savedInstanceState);
       ARouter.getInstance().inject(this);
       binding.frgmentHomeFab.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               MLog.e("Login:"+accountService.isLogin());
               MLog.e("AccountId:"+accountService.getAccountId());

          }
      });
  }
}

img 运行结果:

E/-SCC-: AccountServiceImpl.isLogin
E/-SCC-: Login:true
E/-SCC-: AccountServiceImpl.getAccountId
E/-SCC-: AccountId:1000

🔥 七、总结

本文介绍了组件化、组件分层、解决了组件的独立调试、集成调试、页面跳转、组件通信等。

其实会了这些后你基本可以搭建自己的组件化项目了。其实最大的问题还是分组分层、组件划分。这个就需要根据你的实际情况来设置。

本项目比较糙,后面会慢慢完善。比如添加Gilde、添加MMVK、添加Room等。

项目传送门

💥 相关推荐

Android OkHttp+Retrofit+Rxjava+Hilt实现网络请求框架

💥 参考与感谢

“终于懂了” 系列:Android组件化,全面掌握!

Android 组件化最佳实践

手把手带你搭建一个优秀的Android项目架构


作者:Android帅次
来源:https://juejin.cn/post/7033954652315975688


收起阅读 »

Unable to extract the trust manager on Android10Platform 完美解决

Unable to extract the trust manager on Android10Platform网上有大致有两种解决方案,但都不靠谱。产生这个异常的根本原因是:builder.sslSocketFactory(sslContext.getSoc...
继续阅读 »

Unable to extract the trust manager on Android10Platform

网上有大致有两种解决方案,但都不靠谱。产生这个异常的根本原因是:

builder.sslSocketFactory(sslContext.getSocketFactory());
这个方式已经过时了,需要新的方式,如下:



final X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {

}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new X509TrustManager[]{trustManager}, new SecureRandom());

OkHttpClient.Builder builder = new OkHttpClient().newBuilder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15,TimeUnit.SECONDS)
.addInterceptor(logInterceptor)
.sslSocketFactory(sslContext.getSocketFactory(),trustManager)
.hostnameVerifier(new HostnameVerifier() {

@Override
public boolean verify(String hostname, SSLSession session) {

return true;
}
});

收起阅读 »

国内知名Wchat团队荣誉出品顶级IM通讯聊天系统

iOS
国内知名Wchat团队荣誉出品顶级IM通讯聊天系统团队言语在先:想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/)。想购买劣质低等产品者勿扰(行业鱼龙混杂,想购买类似低能协议xmpp者勿扰)。想购买由类似ope...
继续阅读 »



国内知名Wchat团队荣誉出品顶级IM通讯聊天系统



团队言语在先:

想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/)

。想购买劣质低等产品者勿扰(行业鱼龙混杂,想购买类似低能协议xmpp者勿扰)

。想购买由类似openfire第三方开源改造而来的所谓第三方通信server者勿扰

。想购买没有做任何安全加密场景者勿扰(随便一句api 一个接口就构成了红包收发/转账/密码设置等没有任何安全系数可言的低质产品)

。想购买非运营级别通信系统勿扰(到处呼喊:最稳定/真正可靠/大并发/真正安全!所有一切都需要实际架构支撑以及理论数值测验)

。想购买无保障/无支撑者勿扰(1W/4W/10W低质产品不可谓没有,必须做到:大并发支持合同保障/合作支持运维保障/在线人数支持架构保障)

。想购买消息丢包者勿扰(满天飞的所谓消息确认机制,最简单的测验既是前端支持消息收发demo测试环境,低质产品一秒收发百条消息必丢必崩,

别提秒发千条/万条,更低质产品可测验:同时发九张图片/根据数字12345678910发送出去,必丢!android vs ios)

。想购买大容量群uer者勿扰(随便宣传既是万人大群/几千大群/群组无限,小团队产品群组上线用户超过4000群消息体量不用很大手机前端必卡)

。最重要一点:口口声声说要运营很大的系统 却想出十几个money的人群勿扰,买产品做系统一要稳定二要长久用三要抛开运维烦恼,预算有限那就干脆

别买,买了几万的系统你一样后面用不起来会烂掉!

。产品体系包括:android ios server adminweb maintenance httpapi h5 webpc (支持server压测/前端消息收发压测/httpapi压测)

。。支持源码,但需要您拿去做一个伟大的系统出来!

。。团队产品目前国内没有同质化,客户集中在国外,有求高质量产品的个人或团队可通过以下方式联系到我们(低价者勿扰!)

。。。球球:383189941 q 513275129

。。。。产品不多介绍直接加我 测试产品更直接

。。。。。创新从未停止 更新不会终止 大陆唯一一家支持大并发保障/支持合同费用包含运维支撑的团队 

收起阅读 »

【Gradle7.0】依赖统一管理的全新方式,了解一下~

前言 随着项目的不断发展,项目中的依赖也越来越多,有时可能会有几百个,这个时候对项目依赖做一个统一的管理很有必要,我们一般会有以下需求: 项目依赖统一管理,在单独文件中配置 不同Module中的依赖版本号统一 不同项目中的依赖版本号统一 ...
继续阅读 »

前言


随着项目的不断发展,项目中的依赖也越来越多,有时可能会有几百个,这个时候对项目依赖做一个统一的管理很有必要,我们一般会有以下需求:



  1. 项目依赖统一管理,在单独文件中配置

  2. 不同Module中的依赖版本号统一

  3. 不同项目中的依赖版本号统一


针对这些需求,目前其实已经有了一些方案:



  1. 使用循环优化Gradle依赖管理

  2. 使用buildSrc管理Gradle依赖

  3. 使用includeBuild统一配置依赖版本


上面的方案支持在不同Module间统一版本号,同时如果需要在项目间共享,也可以做成Gradle插件发布到远端,已经基本可以满足我们的需求
不过Gradle7.0推出了一个新的特性,使用Catalog统一依赖版本,它支持以下特性:



  1. 对所有module可见,可统一管理所有module的依赖

  2. 支持声明依赖bundles,即总是一起使用的依赖可以组合在一起

  3. 支持版本号与依赖名分离,可以在多个依赖间共享版本号

  4. 支持在单独的libs.versions.toml文件中配置依赖

  5. 支持在项目间共享依赖


使用Version Catalog


注意,Catalog仍然是一个孵化中的特性,如需使用,需要在settings.gradle中添加以下内容:


enableFeaturePreview('VERSION_CATALOGS')

从命名上也可以看出,Version Catalog其实就是一个版本的目录,我们可以从目录中选出我们需要的依赖使用
我们可以通过如下方式使用Catalog中声明的依赖


dependencies {
implementation(libs.retrofit)
implementation(libs.groovy.core)
}

在这种情况下,libs是一个目录,retrofit表示该目录中可用的依赖项。 与直接在构建脚本中声明依赖项相比,Version Catalog具有许多优点:



  • 对于每个catalog,Gradle都会生成类型安全的访问器,以便你在IDE中可以自动补全.(注:目前在build.gradle中还不能自动补全,可能是指kts或者开发中?)

  • 声明在catalog中的依赖对所有module可见,当修改版本号时,可以统一管理统一修改

  • catalog支持声明一个依赖bundles,即一些总是一起使用的依赖的组合

  • catalog支持版本号与依赖名分离,可以在多个依赖间共享版本号


声明Version Catalog


Version Catalog可以在settings.gradle(.kts)文件中声明。


dependencyResolutionManagement {
versionCatalogs {
libs {
alias('retrofit').to('com.squareup.retrofit2:retrofit:2.9.0')
alias('groovy-core').to('org.codehaus.groovy:groovy:3.0.5')
alias('groovy-json').to('org.codehaus.groovy:groovy-json:3.0.5')
alias('groovy-nio').to('org.codehaus.groovy:groovy-nio:3.0.5')
alias('commons-lang3').to('org.apache.commons', 'commons-lang3').version {
strictly '[3.8, 4.0['
prefer '3.9'
}
}
}
}

别名必须由一系列以破折号(-,推荐)、下划线 (_) 或点 (.) 分隔的标识符组成。
标识符本身必须由ascii字符组成,最好是小写,最后是数字。


值得注意的是,groovy-core会被映射成libs.groovy.core
如果你想避免映射可以使用大小写来区分,比如groovyCore会被处理成libs.groovyCore


具有相同版本号的依赖


在上面的示例中,我们可以看到三个groovy依赖具有相同的版本号,我们可以把它们统一起来


dependencyResolutionManagement {
versionCatalogs {
libs {
version('groovy', '3.0.5')
version('compilesdk', '30')
version('targetsdk', '30')
alias('groovy-core').to('org.codehaus.groovy', 'groovy').versionRef('groovy')
alias('groovy-json').to('org.codehaus.groovy', 'groovy-json').versionRef('groovy')
alias('groovy-nio').to('org.codehaus.groovy', 'groovy-nio').versionRef('groovy')
alias('commons-lang3').to('org.apache.commons', 'commons-lang3').version {
strictly '[3.8, 4.0['
prefer '3.9'
}
}
}
}

除了在依赖中,我们同样可以在build.gradle中获取版本,比如可以用来指定compileSdk


android {
compileSdk libs.versions.compilesdk.get().toInteger()


defaultConfig {
applicationId "com.zj.gradlecatalog"
minSdk 21
targetSdk libs.versions.targetsdk.get().toInteger()
}
}

如上,可以使用catalog统一compileSdk,targetSdk,minSdk的版本号


依赖bundles


因为在不同的项目中经常系统地一起使用某些依赖项,所以Catalog提供了bundle(依赖包)的概念。依赖包基本上是几个依赖项打包的别名。
例如,你可以这样使用一个依赖包,而不是像上面那样声明 3 个单独的依赖项:


dependencies {
implementation libs.bundles.groovy
}

groovy依赖包声明如下:


dependencyResolutionManagement {
versionCatalogs {
libs {
version('groovy', '3.0.5')
version('checkstyle', '8.37')
alias('groovy-core').to('org.codehaus.groovy', 'groovy').versionRef('groovy')
alias('groovy-json').to('org.codehaus.groovy', 'groovy-json').versionRef('groovy')
alias('groovy-nio').to('org.codehaus.groovy', 'groovy-nio').versionRef('groovy')
alias('commons-lang3').to('org.apache.commons', 'commons-lang3').version {
strictly '[3.8, 4.0['
prefer '3.9'
}
bundle('groovy', ['groovy-core', 'groovy-json', 'groovy-nio'])
}
}
}

如上所示:添加groovy依赖包等同于添加依赖包下的所有依赖项


插件版本


除了Library之外,Catalog还支持声明插件版本。
因为library由它们的groupartifactversion表示,但Gradle插件仅由它们的idversion标识。
因此,插件需要单独声明:


dependencyResolutionManagement {
versionCatalogs {
libs {
alias('jmh').toPluginId('me.champeau.jmh').version('0.6.5')
}
}
}

然后可以在plugins块下面使用


plugins {
id 'java-library'
id 'checkstyle'
// 使用声明的插件
alias(libs.plugins.jmh)
}

在单独文件中配置Catalog


除了在settings.gradle中声明Catalog外,也可以通过一个单独的文件来配置Catalog
如果在根构建的gradle目录中找到了libs.versions.toml文件,则将使用该文件的内容自动声明一个Catalog


TOML文件主要由4个部分组成:



  • [versions] 部分用于声明可以被依赖项引用的版本

  • [libraries] 部分用于声明Library的别名

  • [bundles] 部分用于声明依赖包

  • [plugins] 部分用于声明插件


如下所示:


[versions]
groovy = "3.0.5"
checkstyle = "8.37"
compilesdk = "30"
targetsdk = "30"

[libraries]
retrofit = "com.squareup.retrofit2:retrofit:2.9.0"
groovy-core = { module = "org.codehaus.groovy:groovy", version.ref = "groovy" }
groovy-json = { module = "org.codehaus.groovy:groovy-json", version.ref = "groovy" }
groovy-nio = { module = "org.codehaus.groovy:groovy-nio", version.ref = "groovy" }
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version = { strictly = "[3.8, 4.0[", prefer="3.9" } }

[bundles]
groovy = ["groovy-core", "groovy-json", "groovy-nio"]

[plugins]
jmh = { id = "me.champeau.jmh", version = "0.6.5" }

如上所示,依赖可以定义成一个字符串,也可以将moduleversion分离开来
其中versions可以定义成一个字符串,也可以定义成一个范围,详情可参见rich-version


[versions]
my-lib = { strictly = "[1.0, 2.0[", prefer = "1.2" }

在项目间共享Catalog


Catalog不仅可以在项目内统一管理依赖,同样可以实现在项目间共享
例如我们需要在团队内制定一个依赖规范,不同组的不同项目需要共享这些依赖,这是个很常见的需求


通过文件共享


Catalog支持通过从Toml文件引入依赖,这就让我们可以通过指定文件路径来实现共享依赖
如下所示,我们在settins.gradle中配置如下:


dependencyResolutionManagement {
versionCatalogs {
libs {
from(files("../gradle/libs.versions.toml"))
}
}
}

此技术可用于声明来自不同文件的多个目录:


dependencyResolutionManagement {
versionCatalogs {
// 声明一个'testLibs'目录, 从'test-libs.versions.toml'文件中
testLibs {
from(files('gradle/test-libs.versions.toml'))
}
}
}

发布插件实现共享


虽然从本地文件导入Catalog很方便,但它并没有解决在组织或外部消费者中共享Catalog的问题。
我们还可能通过Catalog插件来发布目录,这样用户直接引入这个插件即可


Gradle提供了一个Catalog插件,它提供了声明然后发布Catalog的能力。


1. 首先引入两个插件


plugins {
id 'version-catalog'
id 'maven-publish'
}

然后,此插件将公开可用于声明目录的catalog扩展


2. 定义目录


上面引入插件后,即可使用catalog扩展定义目录


catalog {
// 定义目录
versionCatalog {
from files('../libs.versions.toml')
}
}

然后可以通过maven-publish插件来发布目录


3. 发布目录


publishing {
publications {
maven(MavenPublication) {
groupId = 'com.zj.catalog'
artifactId = 'catalog'
version = '1.0.0'
from components.versionCatalog
}
}
}

我们定义好groupId,artifactId,version,from就可以发布了
我们这里发布到mavenLocal,你也可以根据需要配置发布到自己的maven
以上发布的所有代码可见:Catalog发布相关代码


4. 使用目录


因为我们已经发布到了mavenLocal,在仓库中引入mavenLocal就可以使用插件了


# settings.gradle
dependencyResolutionManagement {
//...
repositories {
mavenLocal()
//...
}
}

enableFeaturePreview('VERSION_CATALOGS')
dependencyResolutionManagement {
versionCatalogs {
libs {
from("com.zj.catalog:catalog:1.0.0")
// 我们也可以重写覆盖catalog中的groovy版本
version("groovy", "3.0.6")
}
}
}

如上就成功引入了插件,就可以使用catalog中的依赖了
这样就完成了依赖的项目间共享,以上使用的所有代码可见:Catalog使用相关代码


总结


项目间共享依赖是比较常见的需求,虽然我们也可以通过自定义插件实现,但还是不够方便
Gradle官方终于推出了Catalog,让我们可以方便地实现依赖的共享,Catalog主要具有以下特性:



  1. 对所有module可见,可统一管理所有module的依赖

  2. 支持声明依赖bundles,即总是一起使用的依赖可以组合在一起

  3. 支持版本号与依赖名分离,可以在多个依赖间共享版本号

  4. 支持在单独的libs.versions.toml文件中配置依赖

  5. 支持在项目间共享依赖


本文所有相关代码


Catalog发布相关代码
Catalog使用相关代码


参考资料



Sharing dependency versions between projects

收起阅读 »

国内知名Wchat团队荣誉出品顶级IM通讯聊天系统

iOS
国内知名Wchat团队荣誉出品顶级IM通讯聊天系统团队言语在先:想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/)。想购买劣质低等产品者勿扰(行业鱼龙混杂,想购买类似低能协议xmpp者勿扰)。想购买由类似ope...
继续阅读 »



国内知名Wchat团队荣誉出品顶级IM通讯聊天系统



团队言语在先:

想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/)

。想购买劣质低等产品者勿扰(行业鱼龙混杂,想购买类似低能协议xmpp者勿扰)

。想购买由类似openfire第三方开源改造而来的所谓第三方通信server者勿扰

。想购买没有做任何安全加密场景者勿扰(随便一句api 一个接口就构成了红包收发/转账/密码设置等没有任何安全系数可言的低质产品)

。想购买非运营级别通信系统勿扰(到处呼喊:最稳定/真正可靠/大并发/真正安全!所有一切都需要实际架构支撑以及理论数值测验)

。想购买无保障/无支撑者勿扰(1W/4W/10W低质产品不可谓没有,必须做到:大并发支持合同保障/合作支持运维保障/在线人数支持架构保障)

。想购买消息丢包者勿扰(满天飞的所谓消息确认机制,最简单的测验既是前端支持消息收发demo测试环境,低质产品一秒收发百条消息必丢必崩,

别提秒发千条/万条,更低质产品可测验:同时发九张图片/根据数字12345678910发送出去,必丢!android vs ios)

。想购买大容量群uer者勿扰(随便宣传既是万人大群/几千大群/群组无限,小团队产品群组上线用户超过4000群消息体量不用很大手机前端必卡)

。最重要一点:口口声声说要运营很大的系统 却想出十几个money的人群勿扰,买产品做系统一要稳定二要长久用三要抛开运维烦恼,预算有限那就干脆

别买,买了几万的系统你一样后面用不起来会烂掉!

。产品体系包括:android ios server adminweb maintenance httpapi h5 webpc (支持server压测/前端消息收发压测/httpapi压测)

。。支持源码,但需要您拿去做一个伟大的系统出来!

。。团队产品目前国内没有同质化,客户集中在国外,有求高质量产品的个人或团队可通过以下方式联系到我们(低价者勿扰!)

。。。球球:383189941 q 513275129

。。。。产品不多介绍直接加我 测试产品更直接

。。。。。创新从未停止 更新不会终止 大陆唯一一家支持大并发保障/支持合同费用包含运维支撑的团队 

收起阅读 »

快来为你的照片添加个性标签吧!

搜索问题、话题或人… 问题 文章 代码 视频 活动· · ·ydhjhs发起Android快来为你的照片添加个性标签吧! 前言 需求图.png PS:最近在项目执行过程中有这样一个需求,要求拍完照的图片必须达到以上的效果。需求分析: 使用用预览布局Surfa...
继续阅读 »

搜索问题、话题或人…
问题
文章
代码
视频
活动
· · ·
ydhjhs
发起
Android
快来为你的照片添加个性标签吧!


  1. 前言

需求图.png


PS:最近在项目执行过程中有这样一个需求,要求拍完照的图片必须达到以上的效果。需求分析:


使用用预览布局SurfaceView,在不局上方使用控件的方式来进行设计,最后通过截图的方式将画面进行保存。


使用图片添加水印的方式来完成。



  1. 方法1 使用SurfaceView

我心想这不简单吗?于是开始一顿balabala的操作,结果到最后一步时发现,SurfaceView居然不能进行截图,截图下来的图片居然是一张黑色的。简单地说这是因为SurfaceView的特性决定的,我们知道安卓中唯一可以在子线程中进行绘制的view就只有Surfaceview了。他可以独立于子线程中绘制,不会导致主线程的卡顿,至于造成surfaceView黑屏的原因,可以移步这里
Android视图SurfaceView的实现原理分析。如果非要使用此方式时还是有三种思路来进行解决:
采用三种思路:


  1. 获取源头视频的截图作为SurfaceView的截图


  2. 获取SurfaceView的画布canvas,将canvas保存成Bitmap


  3. 直接截取整个屏幕,然后在截图SurfaceView位置的图



复制代码


但是我觉得这种方式太过繁琐,所以选择用添加水印的式来完成。



  1. 方法2 给拍照下来的图片添加水印

第一步:获取拍照权限








复制代码


这里使用到郭霖大佬的开源库PermissionX获取权限:


PermissionX.init(this)


.permissions(Manifest.permission.CAMERA,  Manifest.permission.RECORD_AUDIO)

.onExplainRequestReason { scope, deniedList ->

val message = "需要您同意以下权限才能正常使用"

scope.showRequestReasonDialog(deniedList, message, "确定", "取消")

}

.request { allGranted, grantedList, deniedList ->

if (allGranted) {

openCamera()

} else {

Toast.makeText(activity, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()

}

}

复制代码


第二步:拍照


android 6.0以后,相机权限需要动态申请。


// 申请相机权限的requestCode


private static final int PERMISSION_CAMERA_REQUEST_CODE = 0x00000012;


/**


* 检查权限并拍照。

* 调用相机前先检查权限。

*/

private void checkPermissionAndCamera() {


   int hasCameraPermission = ContextCompat.checkSelfPermission(getApplication(),

Manifest.permission.CAMERA);

if (hasCameraPermission == PackageManager.PERMISSION_GRANTED) {

//有调起相机拍照。

openCamera();

} else {

//没有权限,申请权限。

ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.CAMERA},

PERMISSION_CAMERA_REQUEST_CODE);

}

}


/**


* 处理权限申请的回调。

*/

@Override


public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {


   if (requestCode == PERMISSION_CAMERA_REQUEST_CODE) {

if (grantResults.length > 0

&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {

//允许权限,有调起相机拍照。

openCamera();

} else {

//拒绝权限,弹出提示框。

Toast.makeText(this,"拍照权限被拒绝",Toast.LENGTH_LONG).show();

}

}

}


复制代码


调用相机进行拍照


申请权限后,就可以调起相机拍照了。调用相机只需要调用startActivityForResult传一个Intent就可以了,但是这个Intent需要传递一个uri,用于保存拍出来的图片,创建这个uri时,各个Android版本有所不同,需要进行版本兼容。


//用于保存拍照图片的uri


private Uri mCameraUri;



// 用于保存图片的文件路径,Android 10以下使用图片路径访问图片

private String mCameraImagePath;



// 是否是Android 10以上手机

private boolean isAndroidQ = Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q;



/**

* 调起相机拍照

*/

private void openCamera() {

Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

// 判断是否有相机

if (captureIntent.resolveActivity(getPackageManager()) != null) {

File photoFile = null;

Uri photoUri = null;



if (isAndroidQ) {

// 适配android 10

photoUri = createImageUri();

} else {

try {

photoFile = createImageFile();

} catch (IOException e) {

e.printStackTrace();

}



if (photoFile != null) {

mCameraImagePath = photoFile.getAbsolutePath();

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {

//适配Android 7.0文件权限,通过FileProvider创建一个content类型的Uri

photoUri = FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", photoFile);

} else {

photoUri = Uri.fromFile(photoFile);

}

}

}



mCameraUri = photoUri;

if (photoUri != null) {

captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);

captureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

startActivityForResult(captureIntent, CAMERA_REQUEST_CODE);

}

}

}



/**

* 创建图片地址uri,用于保存拍照后的照片 Android 10以后使用这种方法

*/

private Uri createImageUri() {

String status = Environment.getExternalStorageState();

// 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储

if (status.equals(Environment.MEDIA_MOUNTED)) {

return getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new ContentValues());

} else {

return getContentResolver().insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, new ContentValues());

}

}



/**

* 创建保存图片的文件

*/

private File createImageFile() throws IOException {

String imageName = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());

File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);

if (!storageDir.exists()) {

storageDir.mkdir();

}

File tempFile = new File(storageDir, imageName);

if (!Environment.MEDIA_MOUNTED.equals(EnvironmentCompat.getStorageState(tempFile))) {

return null;

}

return tempFile;

}

复制代码


接收拍照结果


@Override


protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {

super.onActivityResult(requestCode, resultCode, data);

if (requestCode == CAMERA_REQUEST_CODE) {

if (resultCode == RESULT_OK) {

if (isAndroidQ) {

// Android 10 使用图片uri加载

ivPhoto.setImageURI(mCameraUri);

} else {

// 使用图片路径加载

ivPhoto.setImageBitmap(BitmapFactory.decodeFile(mCameraImagePath));

}

} else {

Toast.makeText(this,"取消",Toast.LENGTH_LONG).show();

}

}

}

复制代码


注意:


这两需要说明一下,Android 10由于文件权限的关系,显示手机储存卡里的图片不能直接使用图片路径,需要使用图片uri加载。


另外虽然我在这里对Android 10和10以下的手机使用了不同的方式创建uri 和加载图片,但其实Android 10创建uri的方式和使用uri加载图片的方式在10以下的手机是同样适用的。
android 7.0需要配置文件共享。

android:name="androidx.core.content.FileProvider"

android:authorities="${applicationId}.fileprovider"

android:exported="false"

android:grantUriPermissions="true">


android:name="android.support.FILE_PROVIDER_PATHS"

android:resource="@xml/file_paths" />



复制代码


在res目录下创建文件夹xml ,放置一个文件file_paths.xml(文件名可以随便取),配置需要共享的文件目录,也就是拍照图片保存的目录。


<?xml version=”1.0” encoding=”utf-8”?>









name="images"

path="Pictures" />





复制代码


第三步:给拍照后得到的图片添加水印


@Override


protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {

super.onActivityResult(requestCode, resultCode, data);

if (requestCode == CAMERA_REQUEST_CODE) {

if (resultCode == RESULT_OK) {

Bitmap mp;

if (isAndroidQ) {

// Android 10 使用图片uri加载

mp = MediaStore.Images.Media.getBitmap(this.contentResolver, t.uri);

} else {

// Android 10 以下使用图片路径加载

mp = BitmapFactory.decodeFile(uri);

}

//对图片添加水印 这里添加一张图片为示例:

ImageUtil.drawTextToLeftTop(this,mp,"示例文字",30,R.color.black,20,30)

} else {

Toast.makeText(this,"取消",Toast.LENGTH_LONG).show();

}

}

}

复制代码


这里使用到一个ImageUtil工具类,我在这里贴上。如果需要使用可以直接拿走~


public class ImageUtil {


/**

* 设置水印图片在左上角

*

* @param context 上下文

* @param src

* @param watermark

* @param paddingLeft

* @param paddingTop

* @return

*/

public static Bitmap createWaterMaskLeftTop(Context context, Bitmap src, Bitmap watermark, int paddingLeft, int paddingTop) {

return createWaterMaskBitmap(src, watermark,

dp2px(context, paddingLeft), dp2px(context, paddingTop));

}



private static Bitmap createWaterMaskBitmap(Bitmap src, Bitmap watermark, int paddingLeft, int paddingTop) {

if (src == null) {

return null;

}

int width = src.getWidth();

int height = src.getHeight();

//创建一个bitmap

Bitmap newb = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);// 创建一个新的和SRC长度宽度一样的位图

//将该图片作为画布

Canvas canvas = new Canvas(newb);

//在画布 0,0坐标上开始绘制原始图片

canvas.drawBitmap(src, 0, 0, null);

//在画布上绘制水印图片

canvas.drawBitmap(watermark, paddingLeft, paddingTop, null);

// 保存

canvas.save(Canvas.ALL_SAVE_FLAG);

// 存储

canvas.restore();

return newb;

}



/**

* 设置水印图片在右下角

*

* @param context 上下文

* @param src

* @param watermark

* @param paddingRight

* @param paddingBottom

* @return

*/

public static Bitmap createWaterMaskRightBottom(Context context, Bitmap src, Bitmap watermark, int paddingRight, int paddingBottom) {

return createWaterMaskBitmap(src, watermark,

src.getWidth() - watermark.getWidth() - dp2px(context, paddingRight),

src.getHeight() - watermark.getHeight() - dp2px(context, paddingBottom));

}



/**

* 设置水印图片到右上角

*

* @param context

* @param src

* @param watermark

* @param paddingRight

* @param paddingTop

* @return

*/

public static Bitmap createWaterMaskRightTop(Context context, Bitmap src, Bitmap watermark, int paddingRight, int paddingTop) {

return createWaterMaskBitmap(src, watermark,

src.getWidth() - watermark.getWidth() - dp2px(context, paddingRight),

dp2px(context, paddingTop));

}



/**

* 设置水印图片到左下角

*

* @param context

* @param src

* @param watermark

* @param paddingLeft

* @param paddingBottom

* @return

*/

public static Bitmap createWaterMaskLeftBottom(Context context, Bitmap src, Bitmap watermark, int paddingLeft, int paddingBottom) {

return createWaterMaskBitmap(src, watermark, dp2px(context, paddingLeft),

src.getHeight() - watermark.getHeight() - dp2px(context, paddingBottom));

}



/**

* 设置水印图片到中间

*

* @param src

* @param watermark

* @return

*/

public static Bitmap createWaterMaskCenter(Bitmap src, Bitmap watermark) {

return createWaterMaskBitmap(src, watermark,

(src.getWidth() - watermark.getWidth()) / 2,

(src.getHeight() - watermark.getHeight()) / 2);

}



/**

* 给图片添加文字到左上角

*

* @param context

* @param bitmap

* @param text

* @return

*/

public static Bitmap drawTextToLeftTop(Context context, Bitmap bitmap, String text, int size, int color, int paddingLeft, int paddingTop) {

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

paint.setColor(color);

paint.setTextSize(dp2px(context, size));

Rect bounds = new Rect();

paint.getTextBounds(text, 0, text.length(), bounds);

return drawTextToBitmap(context, bitmap, text, paint, bounds,

dp2px(context, paddingLeft),

dp2px(context, paddingTop) + bounds.height());

}



/**

* 绘制文字到右下角

*

* @param context

* @param bitmap

* @param text

* @param size

* @param color

* @return

*/

public static Bitmap drawTextToRightBottom(Context context, Bitmap bitmap, String text, int size, int color, int paddingRight, int paddingBottom) {

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

paint.setColor(color);

paint.setTextSize(dp2px(context, size));

Rect bounds = new Rect();

paint.getTextBounds(text, 0, text.length(), bounds);

return drawTextToBitmap(context, bitmap, text, paint, bounds,

bitmap.getWidth() - bounds.width() - dp2px(context, paddingRight),

bitmap.getHeight() - dp2px(context, paddingBottom));

}



/**

* 绘制文字到右上方

*

* @param context

* @param bitmap

* @param text

* @param size

* @param color

* @param paddingRight

* @param paddingTop

* @return

*/

public static Bitmap drawTextToRightTop(Context context, Bitmap bitmap, String text, int size, int color, int paddingRight, int paddingTop) {

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

paint.setColor(color);

paint.setTextSize(dp2px(context, size));

Rect bounds = new Rect();

paint.getTextBounds(text, 0, text.length(), bounds);

return drawTextToBitmap(context, bitmap, text, paint, bounds,

bitmap.getWidth() - bounds.width() - dp2px(context, paddingRight),

dp2px(context, paddingTop) + bounds.height());

}



/**

* 绘制文字到左下方

*

* @param context

* @param bitmap

* @param text

* @param size

* @param color

* @param paddingLeft

* @param paddingBottom

* @return

*/

public static Bitmap drawTextToLeftBottom(Context context, Bitmap bitmap, String text, int size, int color, int paddingLeft, int paddingBottom) {

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

paint.setColor(color);

paint.setTextSize(dp2px(context, size));

Rect bounds = new Rect();

paint.getTextBounds(text, 0, text.length(), bounds);

return drawTextToBitmap(context, bitmap, text, paint, bounds,

dp2px(context, paddingLeft),

bitmap.getHeight() - dp2px(context, paddingBottom));

}



/**

* 绘制文字到中间

*

* @param context

* @param bitmap

* @param text

* @param size

* @param color

* @return

*/

public static Bitmap drawTextToCenter(Context context, Bitmap bitmap, String text, int size, int color) {

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

paint.setColor(color);

paint.setTextSize(dp2px(context, size));

Rect bounds = new Rect();

paint.getTextBounds(text, 0, text.length(), bounds);

return drawTextToBitmap(context, bitmap, text, paint, bounds,

(bitmap.getWidth() - bounds.width()) / 2,

(bitmap.getHeight() + bounds.height()) / 2);

}



//图片上绘制文字

private static Bitmap drawTextToBitmap(Context context, Bitmap bitmap, String text, Paint paint, Rect bounds, int paddingLeft, int paddingTop) {

android.graphics.Bitmap.Config bitmapConfig = bitmap.getConfig();



paint.setDither(true); // 获取跟清晰的图像采样

paint.setFilterBitmap(true);// 过滤一些

if (bitmapConfig == null) {

bitmapConfig = android.graphics.Bitmap.Config.ARGB_8888;

}

bitmap = bitmap.copy(bitmapConfig, true);

Canvas canvas = new Canvas(bitmap);



canvas.drawText(text, paddingLeft, paddingTop, paint);

return bitmap;

}



/**

* 缩放图片

*

* @param src

* @param w

* @param h

* @return

*/

public static Bitmap scaleWithWH(Bitmap src, double w, double h) {

if (w == 0 || h == 0 || src == null) {

return src;

} else {

// 记录src的宽高

int width = src.getWidth();

int height = src.getHeight();

// 创建一个matrix容器

Matrix matrix = new Matrix();

// 计算缩放比例

float scaleWidth = (float) (w / width);

float scaleHeight = (float) (h / height);

// 开始缩放

matrix.postScale(scaleWidth, scaleHeight);

// 创建缩放后的图片

return Bitmap.createBitmap(src, 0, 0, width, height, matrix, true);

}

}



/**

* dip转pix

*

* @param context

* @param dp

* @return

*/

public static int dp2px(Context context, float dp) {

final float scale = context.getResources().getDisplayMetrics().density;

return (int) (dp * scale + 0.5f);

}

}


复制代码



  1. 最终实现的效果如下:

效果.jpg


5.总结


整体来说没有什么太大的问题,添加水印的原理就是通过Canvas绘制的方式将文字/图片添加到图片上。最后再将修改之后的图片呈现给用户。同时也记录下SurfaceView截图黑屏的问题。


作者:LiChengZe_Blog
链接:https://juejin.cn/post/6960579316191068197
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 收藏 分享 举报 2021-05-11
0 个评论
ydhjhs
写下你的评论…
发起人
柳天明柳天明
推荐内容
java设计模式:原型模式
算法与数据结构之数组
算法与数据结构之算法复杂度
算法与数据结构之链表
java设计模式:抽象工厂模式
java 设计模式:责任链模式与Android事件传递
java 设计模式:观察者
java 设计模式:模版方法
java 设计模式:策略模式
java 设计模式:工厂方法模式
一个开放、互助、协作、创意的社区
关于imGeek关于专职工程师值守社区财富榜赞助商友情链接热门标签
京ICP备14026002号-3
收起阅读 »

安卓 客服云 企业欢迎语集成

1.后台配置企业欢迎语   管理员模式---设置--功能设置  企业欢迎语     2.代码   ChatClient.getInstance().chatManager().getEnterpriseWelcome(new ValueCallBack...
继续阅读 »
1.后台配置企业欢迎语
  管理员模式---设置--功能设置  企业欢迎语 
  



2.代码
 

ChatClient.getInstance().chatManager().getEnterpriseWelcome(new ValueCallBack() {
@Override
public void onSuccess(String value) {
Log.i("TAG value", value);
String enterpriseWelcome=value;
if (!TextUtils.isEmpty(value)) {
ChatClient.getInstance().chatManager().getCurrentSessionId(toChatUsername, new ValueCallBack() {//toChatUsername替换为自己的IM服务号
@Override
public void onSuccess(String value) {
Log.e("TAG value:", value + " 当返回value不为空时,则返回的当前会话的会话ID,也就是说会话正在咨询中,不需要发送欢迎语");
if (value.isEmpty()) {//
Message message = Message.createReceiveMessage(Message.Type.TXT);

EMTextMessageBody body = null;

body = new EMTextMessageBody(enterpriseWelcome);

message.setFrom(toChatUsername);//toChatUsername替换为自己的IM服务号

message.addBody(body);

message.setMsgTime(System.currentTimeMillis());

message.setStatus(Message.Status.SUCCESS);

message.setMsgId(UUID.randomUUID().toString());

ChatClient.getInstance().chatManager().saveMessage(message);

messageList.refresh();
}
}

@Override
public void onError(int error, String errorMsg) {

}
});
}

}

@Override
public void onError(int error, String errorMsg) {

}
});












收起阅读 »