wuhaiyang.github.io's People
wuhaiyang.github.io's Issues
8大排序算法图文讲解
Https 为什么是安全的?
chmod日常记录
查看文件权限
ls -l
文件类型&权限 | 文件数量 | 所属用户 | 所在群组 | 文件大小 | 修改日期 | 文件名称 |
---|---|---|---|---|---|---|
-rwxr--r-- | 1 | user | staff | 4.9K | Nov 27 18:59 | gradlew.properties |
第一个字符: d 代表文件夹, - 代表非目录类型;后面九个字符分三组,rwx 代表 可读/可写/可执行,没用相应的权限使用 - 替代 |
文件数量 | user 当前用户 | staff | 文件大小 | 修改日期 | 文件名称 |
修改文件权限
chmod 用户+操作+权限 文件
用户 | 操作 | 权限 |
---|---|---|
u->user,g->group,o->other,a->all | + : 增加权限, -:取消权限, =:赋值权限 | r:读,w:写,x:执行 |
例如:
chmod go-x startup.sh
代表将拥有者所在群组和其他用户权限取消可执行权限
还有一种简答的写法
1 表示可执行,2 表示可写,4 表示可读
修改所有用户访问权限未可读可写可执行的话 使用
chmod 777 startup.sh
三个数字从前到后分别表示 u、g、o 三种用户类型的访问权限,使用时按需修改。
有时候需要递归修改目录文件及其子目录中的文件类型,可以使用 -R 选项
chmod -R 777 *
让当前目录下的所有文件都具备可读可写可操作权限
2016 年度总结
加快gradle构建速度
注意,以下所有配置视情况而定
- gradle.properties
org.gradle.jvmargs=-Xmx4608M
android.useDeprecatedNdk=true
# 并行构建
org.gradle.parallel=true
# 开启Gradle守护进程
org.gradle.daemon=true
org.gradle.caching=true
- build.gradle
android{
//PNG优化在默认情况下是打开的,开发模式下禁用
if (project.hasProperty('devBuild')){
aaptOptions.cruncherEnabled = false
}
}
tasks.whenTaskAdded { task ->
if (task.name.contains('AndroidTest') || task.name.contains('Test')) {
task.enabled = false
}
}
具体可参考https://github.com/halohoop/SpeedUpGradleBuild2项目
gradle build --profile
查看编译报告文件,build/reports/profile下就会出现一个html文件
Gradle 引用本地AAR
把 aar 文件放在一个或多个文件目录中,比如 meiqiasdk-release.aar 就放在 libs 目录内
在 app 的 build.gradle 文件android{}添加如下内容
repositories {
flatDir {
dirs 'libs'
// 多个目录以逗号分隔,相对路径和绝对路径都可以
// dirs '../../Meiqia_SDK_For_Android/meiqia/build/outputs/aar', '../../Meiqia_SDK_For_Android/meiqiasdk/build/outputs/aar'
}
}
之后在其他项目中添加一句 gradle 依赖便方便的引用了该 library
compile(name:'meiqia-release', ext:'aar')
compile(name:'meiqiasdk-release', ext:'aar')
gradle 编译错误修复
###编译错误
Android Error:Execution failed for task ':app:compileDebugJavaWithJavac'
如果在
./gradle clean build --info
编译后从输出日志仍然看不出具体哪出代码报错,提供一中解决方案
- 执行
./gradlew -p yourprojectdir compileDebugJavaWithJavac
输入错误日志 - 查看具体哪个类报错,定位目标,锁定原因,修复问题
gradle tools 3.X
关于 implemntion api 和 compile 区别
- implemention 不能传递依赖,只能对当前module生效,不对外公开,对编译速度有所提高
- api 完全等同于 compile 指令,可以传递依赖
未完待续....
现代 JavaScript 参考
关于Firebase 你到底了解多少?
简单介绍 & android 集成相关
多线程编程
线程的状态
-
New
start 方法之前的一些基础工作状态
-
Runable
调用start之后的状态
-
Blocked
线程被锁阻塞,暂时不活动 例如:lock.lock
-
Waiting
线程暂时不活动,需要线程调度器唤醒激活它,例如:condition.await()
-
Timed Waiting
超时等待状态,例如:Thread.sleed(1000),
-
Terminated
终止状态:run方法执行完毕正常退出;未捕获异常而终止了run方法,从而进入终止状态
线程同步
简单理解就是:多个线程抢占统一资源过程
重入锁 和 条件对象
1. 重入锁ReentrantLock
支持一个线程对资源的重复加载
Lock lock = new ReetrantLock();
lock.lock();
try{
...
}catch(Exception e){
} finally {
lock.unlock();
}
2. 条件对象
一个条件对象管理那些已经获得了一个锁但是却不能做有用的工作线程
伪代码:
public void transfer() {
lock.lock();
try {
while(...) {
condition.await(); // 阻塞当前线程 并放弃锁
}
....
condition.signalAll();// 激活因为这一条件而等待的所有线程
} catch(Exception e) {
} finally {
lock.unlock();
}
}
同步方法
synchronized
等价于
Lock lock = new ReetrantLock();
lock.lock();
try{
...
}catch(Exception e){
} finally {
lock.unlock();
}
volatile
Java内存模型
java 内存模型定义了线程和主存之间的抽象关系,线程之间的共享变量存储于主存中,每一个线程都有一个私有本地内存,本地内存存储了该线程共享变量的**副本**,Java 内存模型控制线程之间的通信
- 线程A 与线程B 之间通讯
改变线程A本地内存变量X ——> 主存更新变量X ——> 线程B 到主存重新获取变量X——>刷新线程B本地内存变量X
并发编程的三大特性
-
原子性
一行代码只包括一个操作x = 3; 原子操作 y = x; 包含了两个步骤 :读取x的值 再讲x的值写入工作内存中 x ++; 包含了三个操作:读取x的值,对x的值进行+1,想工作内存写入新值
Java 提供了原子性 java.utl.concurrent.atnmic api:
AtomicBoolean,AtomicLong,AtomicReference
... -
可见性
一个线程修改的状态对另一个线程是可见的
当一个变量被修饰volatile
它会保证修改的值立即被更新到主存,普通变量不能保证可见性,因为普通共享变量被修改之后,并不一定会立即被写入主存的 -
有序性
java 内存模型中允许编译器和处理器对指令进行重排序,虽然重排序不会影响到单线程程序执行的正确性,但是会影响到多线程并发执行的正确性
可以通过volatile Sychronized Lock
保证有序性
Volatile
保证了可见性和有序性,不保证原子性
使用volatile必须具备以下两个条件
- 对变量的写操作不会依赖当前值 -> 自增||自减
- 改变量没有包含在具有其他变量的不变式子
其中条件2 :
vilatile int lower,upper;
public void setLower(int low) {
if (low > upper) {
throw exception
}
lower = low;
}
public void setUpper(int upper) {
if (upper < lower) {
throw exception
}
upper = upper;
}
默认lower upper = (0,5),
线程1 执行:setLower(4);
线程2 执行:setUpper(3);
线程1,2 通知执行 使得最后(4,3) 显然是错误的
使用场景 状态标识 & DCL(双重检查模式)单例
阻塞队列
公平访问队列:阻塞所有的生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列
ArrayBlockingQueue
FIFO,有界
LinkedBlockingQueue
基于链表
FIFO
效率较高
未完待续
Android 系统启动流程
Android 系统启动流程
本文基于android8.0系统源码来分析
系统启动大致流程图分析,一图以蔽之:
其中,启动电源、引导程序Bootloader
、linux
内核启动 部分读者自行了解,重点关注:init
进程启动过程,zygote
进程启动过程,SystemServer
进程启动过程,Launcher
启动过程
init 进程启动过程
init
进程是Android系统创建的第一个进程,其职责之一:创建zygote
进程和属性服务等。
- init 入口函数
init
进程的入口函数是main
system/core/init/init.cpp
源码
int main(int argc, char** argv){
...
if (!strcmp(basename(argv[0]), "watchdogd")) {
return watchdogd_main(argc, argv);
}
...
// 1. 创建文件夹并挂载到 initramdisk
mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");
mkdir("/dev/pts", 0755);
mkdir("/dev/socket", 0755);
mount("devpts", "/dev/pts", "devpts", 0, NULL);
#define MAKE_STR(x) __STRING(x)
mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC));
...
// 2. 初始化属性相关属性
property_init();
process_kernel_dt();
process_kernel_cmdline();
...
// 3. 启动属性服务
start_property_service();
set_usb_controller();
...
Parser& parser = Parser::GetInstance();
...
std::string bootscript = GetProperty("ro.boot.init_rc", "");
if (bootscript.empty()) {
// 4. 解析init.rc 文件
parser.ParseConfig("/init.rc");
parser.set_is_system_etc_init_loaded(
parser.ParseConfig("/system/etc/init"));
parser.set_is_vendor_etc_init_loaded(parser.ParseConfig("/vendor/etc/init"));
parser.set_is_odm_etc_init_loaded(parser.ParseConfig("/odm/etc/init"));
}
...
property_load_boot_defaults();
export_oem_lock_status();
}
init
main
函数做了很多事情,需要重点关注代码中的注释部分。注释4处解析init.rc
-
它是由Android Init Language介绍脚本编写的,它主要包含五种类型语句:
Action
、Commands
、Services
、Options
和Import
,较为重要的语法介绍:on <trigger> [&& <trigger>]* //设置触发器 <command> <command> //动作触发之后要执行的命令
截取init.rc 部分
Action
类型语句代码,如下所示:on init sysclktz 0 copy /proc/cmdline /dev/urandom copy /default.prop /dev/urandom ... mkdir /dev/stune/foreground mkdir /dev/stune/background mkdir /dev/stune/top-app
不难发现,
init.rc
组合了许多系统执行命令。为了分析如何创建zygote
,主要查看下Service
类型语句,格式如下service <name> <pathname> [ <argument> ]* //<service的名字><执行程序路径><传递参数> <option> //option是service的修饰词,影响什么时候、如何启动services <option>
每一个服务对应一个
rc
文件,相应的zygote服务的启动脚本则在init.zygoteXX.rc
中定义,切换到system/core/rootdir/
源码目录,存在多个形似zygote.rc
文件,这里,选取64位处理器为例,也即为system/core/rootdir/init.zygote64.rc
service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server class main //1. zygote 进程的类名 priority -20 user root group root readproc socket zygote stream 660 root system onrestart write /sys/android_power/request_state wake onrestart write /sys/power/state on onrestart restart audioserver onrestart restart cameraserver onrestart restart media onrestart restart netd onrestart restart wificond writepid /dev/cpuset/foreground/tasks
结合
Init
脚本语言的Service
类型规范,可以得出init进程要启动的进程名称为zygote
,zygote
进程执行程序的路径为/system/bin/app_process64
。注释1 后文会用到 -
init进程fork zygote进程
从
zygote
启动脚本init.zygote64.rc
中得出zygote
的class name 为main
。 在init.rc
配置中... on nonencrypted class_start main class_start late_start ...
结合
Android Init Language
脚本Action
类型语句,class_start
是一个COMMAND
,用于启动还没有运行的指定服务,对应的函数是system/core/init/builtins.cpp#do_class_start
static int do_class_start(const std::vector<std::string>& args) { /* Starting a class does not start services * which are explicitly disabled. They must * be started individually. */ ServiceManager::GetInstance(). ForEachServiceInClass(args[1], [] (Service* s) { s->StartIfNotDisabled(); }); // 调用了Service.StartIfNotDisabled return 0; }
system/core/init/service.cpp
的StartIfNotDisabled函数bool Service::StartIfNotDisabled() { if (!(flags_ & SVC_DISABLED)) { return Start(); // 执行Start函数 } else { flags_ |= SVC_DISABLED_START; } return true; }
执行
Start
函数:bool Service::Start() { flags_ &= (~(SVC_DISABLED|SVC_RESTARTING|SVC_RESET|SVC_RESTART|SVC_DISABLED_START)); if (flags_ & SVC_RUNNING) { //1. 如果Service已经启动,则不启动 return false; } ... struct stat sb; // 2. 判断启动的Service对应的执行文件是否存在,不存在则不启动该Service if (stat(args_[0].c_str(), &sb) == -1) { PLOG(ERROR) << "cannot find '" << args_[0] << "', disabling '" << name_ << "'"; flags_ |= SVC_DISABLED; return false; } ... pid_t pid = -1; if (namespace_flags_) { pid = clone(nullptr, nullptr, namespace_flags_ | SIGCHLD, nullptr); } else { // 3. fork 函数创建子进程 pid = fork(); } if (pid == 0) { ... std::vector<char*> strs; ExpandArgs(args_, &strs); // 4. 通过execve 函数执行程序 if (execve(strs[0], (char**) &strs[0], (char**) ENV) < 0) { PLOG(ERROR) << "cannot execve('" << strs[0] << "')"; } } }
注释3处将zygote进程启动; 注释4处,在子进程中调用
execve
函数来执行/system/bin/app_process64
,这样就会进入framework/cmds/app_process/app_main.cpp
源码的main
的函数int main(int argc, char* const argv[]){ ... if (zygote) { // 1. 执行Zygote 进程Java框架层代码 runtime.start("com.android.internal.os.ZygoteInit", args, zygote); } else if (className) { runtime.start("com.android.internal.os.RuntimeInit", args, zygote); } else { fprintf(stderr, "Error: no class name or --zygote supplied.\n"); app_usage(); LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied."); } }
从注释1:调用
AppRuntime#start
执行Zygote 进程Java框架层代码 -
属性服务初始化与启动
init入口函数
system/core/init/init.cpp
源码注释2,property_init()
函数具体实现代码system/core/init/property_service.cpp
void property_init() { if (__system_property_area_init()) { LOG(ERROR) << "Failed to initialize property area"; exit(1); } }
其中
__system_property_area_init
函数用来初始化属性内存区域。init入口函数
system/core/init/init.cpp
源码注释3处start_property_service
函数具体实现代码void start_property_service() { property_set("ro.property_service.version", "2"); // 1. 创建非阻塞的socket property_set_fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0666, 0, 0, NULL); if (property_set_fd == -1) { PLOG(ERROR) << "start_property_service socket creation failed"; exit(1); } // 2. 对property_set_fd 进行监听,第二个参数代表属性服务最多可以同时为8个视图设置属性的用户提供服务 listen(property_set_fd, 8); // 3. 用epoll 监听property_set_fd属性,当property_set_fd属性设置时,init进程将用handle_property_set_fd函数来处理 register_epoll_handler(property_set_fd, handle_property_set_fd); }
在新
linux
内核中,epoll
用来替换select
,epoll
最大的好处在于它不会随着监听fd数目的增长而降低效率。因为内核中的select
实现是采用轮询来处理的,轮询的fd数目越多,自然耗时越多
当属性服务接受到客户端的请求,关注下handle_property_set_fd
函数static void handle_property_set_fd(){ ... switch (cmd) { case PROP_MSG_SETPROP:{ char prop_name[PROP_NAME_MAX]; char prop_value[PROP_VALUE_MAX]; if (!socket.RecvChars(prop_name, PROP_NAME_MAX, &timeout_ms) || !socket.RecvChars(prop_value, PROP_VALUE_MAX, &timeout_ms)) { PLOG(ERROR) << "sys_prop(PROP_MSG_SETPROP): error while reading name/value from the socket"; return; } prop_name[PROP_NAME_MAX-1] = 0; prop_value[PROP_VALUE_MAX-1] = 0; // 1. 处理属性设置 handle_property_set(socket, prop_value, prop_value, true); } } ... } static void handle_property_set(SocketConnection& socket, const std::string& name, const std::string& value, bool legacy_protocol){ ... // 2. 检查权限 if (check_mac_perms(name, source_ctx, &cr)) { // 3. 设置属性 uint32_t result = property_set(name, value); if (!legacy_protocol) { socket.SendUint32(result); } } else { LOG(ERROR) << "sys_prop(" << cmd_name << "): permission denied uid:" << cr.uid << " name:" << name; if (!legacy_protocol) { socket.SendUint32(PROP_ERROR_PERMISSION_DENIED); } } ... } uint32_t property_set(const std::string& name, const std::string& value){ size_t valuelen = value.size(); //1. 校验属性键是否合法 if (!is_legal_property_name(name)) { LOG(ERROR) << "property_set(\"" << name << "\", \"" << value << "\") failed: bad name"; return PROP_ERROR_INVALID_NAME; } //2. 校验值是否合法 if (valuelen >= PROP_VALUE_MAX) { LOG(ERROR) << "property_set(\"" << name << "\", \"" << value << "\") failed: " << "value too long"; return PROP_ERROR_INVALID_VALUE; } //3. 加载属性元信息, prop_info* pi = (prop_info*) __system_property_find(name.c_str()); if (pi != nullptr) { // ro.* properties are actually "write-once". if (android::base::StartsWith(name, "ro.")) { // 属性ro.前缀开头,表示只读,不能修改 LOG(ERROR) << "property_set(\"" << name << "\", \"" << value << "\") failed: " << "property already set"; return PROP_ERROR_READ_ONLY_PROPERTY; } // 更新属性值 __system_property_update(pi, value.c_str(), valuelen); } ... }
上述源码注释,介绍属性修改的过程
-
init进程启动过程总结
大致做了三件事:
- 创建文件目录并挂载设备;
- 初始化和启动属性服务;
- 解析
init.rc
文件,forkzygote
进程
zygote 进程启动过程
zygote
译为“孵化器”,是一个进程名字,DVM、应用程序进程以及运行系统关键服务的SystemServer
进程都是由它创建并启动,其他应用所在的进程都是zygote
进程的子进程
-
AppRuntime 分析
从上文得知init
启动zygote
时主要是调用app_main.cpp
的main
函数中的AppRuntime
的start
函数来启动zygote
服务的:
framework/cmds/app_process/app_main.cpp
int main(int argc, char* const argv[]){ ... AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv)); ... while (i < argc) { const char* arg = argv[i++]; if (strcmp(arg, "--zygote") == 0) { zygote = true; niceName = ZYGOTE_NICE_NAME; } else if (strcmp(arg, "--start-system-server") == 0){ // 1.启动SystemServer命令 startSystemServer = true; } else if (strcmp(arg, "--application") == 0) { application = true; } else if (strncmp(arg, "--nice-name=", 12) == 0) { niceName.setTo(arg + 12); } else if (strncmp(arg, "--", 2) != 0) { className.setTo(arg); break; } else { --i; break; } } if (zygote) { //2. 将args(启动SystemServer命令)作为形参传入 runtime.start("com.android.internal.os.ZygoteInit", args, zygote); } else if (className) { runtime.start("com.android.internal.os.RuntimeInit", args, zygote); } }
main
函数第二个形参中包含--start-system-server
,因为上一节init
进程启动分析中提到过init.zygote64.rc
中Init
脚本:service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server ...
其中
--start-system-server
为携带参数,该参数传递到app_main.cpp#main()
函数 ,AppRuntime#start
启动zygote
进程,同时也会将SystemServer
进程启动#include <android_runtime/AndroidRuntime.h> class AppRuntime : public AndroidRuntime{ ... }
AppRuntime
继承AndroidRuntime
,即调用AndroidRuntime#start
函数
frameworks/base/core/jni/AndroidRuntime.cpp
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote){ ... JniInvocation jni_invocation; jni_invocation.Init(NULL); JNIEnv* env; // 1. 开启虚拟机 if (startVm(&mJavaVM, &env, zygote) != 0) { return; } onVmCreated(env); // 2. 注册android native函数 if (startReg(env) < 0) { ALOGE("Unable to register all android natives\n"); return; } // 3. 将main函数的 形参options 转成 jobjectArray,作为注释5执行className 类的main函数 jclass stringClass; jobjectArray strArray; jstring classNameStr; ... stringClass = env->FindClass("java/lang/String"); strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL); classNameStr = env->NewStringUTF(className); for (size_t i = 0; i < options.size(); ++i) { jstring optionsStr = env->NewStringUTF(options.itemAt(i).string()); assert(optionsStr != NULL); env->SetObjectArrayElement(strArray, i + 1, optionsStr); } char* slashClassName = toSlashClassName(className); jclass startClass = env->FindClass(slashClassName);// 反射获取类实例 ... // 4. 找到className 类对应的main函数 jmethodID startMeth = env->GetStaticMethodID(startClass, "main", "([Ljava/lang/String;)V"); //5. 注释3出生成的strArray 参数数组 传递给className 类的main 函数并执行它 env->CallStaticVoidMethod(startClass, startMeth, strArray); }
从源码
app_main.cpp
得知,源码中提到的className
为com.android.internal.os.ZygoteInit
,而ZygoteInit
是java编写的,所以需要通过JNI的方式完成调用(c++ ->Java 反射实现) -
Zygote的java 框架层
从上一节得知,
zygote
通过jni调用ZygoteInit.java
,
com.android.internal.os.ZygoteInit
public static void main(String argv[]) { ... boolean startSystemServer = false; String socketName = "zygote"; ... for (int i = 1; i < argv.length; i++) { if ("start-system-server".equals(argv[i])) { // 1. 参数中携带start-system-server startSystemServer = true; } else if{...} } // 2. 注册zygote 用的Socket zygoteServer.registerServerSocketFromEnv(socketName); ... if (startSystemServer) { // 3. fork SystemServer进程(用于启动系统关键服务) Runnable r = forkSystemServer(abiList, socketName, zygoteServer); // {@code r == null} in the parent (zygote) process, and {@code r != null} in the // child (system_server) process. if (r != null) { //4. 执行SystemServer#run 方法 r.run(); return; } } }
注释2出的
registerServerSocketFromEnv
用来等待ActivityManagerService
来请求Zygote
创建应用程序进程,注释3、4用于forkSystemServer
进程,和执行run
方法。
com.android.internal.os.ZygoteServer
void registerServerSocketFromEnv(String socketName) { if (mServerSocket == null) { int fileDesc; final String fullSocketName = ANDROID_SOCKET_PREFIX + socketName; try { String env = System.getenv(fullSocketName); fileDesc = Integer.parseInt(env); } catch (RuntimeException ex) { throw new RuntimeException(fullSocketName + " unset or invalid", ex); } try { FileDescriptor fd = new FileDescriptor(); fd.setInt$(fileDesc); // 1. mServerSocket = new LocalServerSocket(fd); mCloseSocketFd = true; } catch (IOException ex) { throw new RuntimeException( "Error binding to local socket '" + fileDesc + "'", ex); } } }
注释1创建
LocalServerSocket
,也就是服务端的Socket, 当Zygote
进程将SystemServer
进程启动后,就会等待ActivityManagerService
请求Zygote
进程来启动新的应用程序进程。接着继续看forkSystemServer
方法private static Runnable forkSystemServer(String abiList, String socketName,ZygoteServer zygoteServer) { ... //1. String args[] = { "--setuid=1000", "--setgid=1000", "--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1023,1024,1032,1065,3001,3002,3003,3006,3007,3009,3010", "--capabilities=" + capabilities + "," + capabilities, "--nice-name=system_server", "--runtime-args", "--target-sdk-version=" + VMRuntime.SDK_VERSION_CUR_DEVELOPMENT, "com.android.server.SystemServer", }; ... //2. pid = Zygote.forkSystemServer( parsedArgs.uid, parsedArgs.gid, parsedArgs.gids, parsedArgs.runtimeFlags, null, parsedArgs.permittedCapabilities, parsedArgs.effectiveCapabilities); //3. if (pid == 0) { if (hasSecondZygote(abiList)) { waitForSecondaryZygote(socketName); } zygoteServer.closeServerSocket(); //4. return handleSystemServerProcess(parsedArgs); } }
注释1处用于为启动
SystemServer
进程参数命令,可以得知启动的类名为com.android.server.SystemServer
,注释2处forkSystemServer
进程,注释3若pid
为0 表示在新创建的子进程中执行,则执行注释4的handleSystemServerProcess
并返回Runnable
对象。由于forkSystemServer
方法调用链较长,这里用UML时序图简略表示:
SystemServer进程启动过程
SystemServer
也是一个进程,从上文得知,它是zygote
fork 出来的。ActivityMangerService
PackageManagerService
WindowManagerService
等这些重要的服务都是通过SystemServer
进程启动的
-
启动服务过程
结合上一节Zygote.forkSystemServer
方法 绘制的UML时序图,最终通过反射的方式调用SystemServer#main
方法
com.android.server.SystemServer
public static void main(String[] args) { // 1. 创建`SystemServer`实例并执行`run`方法,注意,它并不是`Runnable`对象 new SystemServer().run(); } ... private void run() { ... //2.注释2用于初始化系统配置(时区、语言...) SystemClock.setCurrentTimeMillis(EARLIEST_SUPPORTED_TIME); SystemProperties.set("persist.sys.timezone", "GMT"); SystemProperties.set("persist.sys.language", ""); ... // 3. 设置进程优先级并创建主线程`Looper` android.os.Process.setThreadPriority( android.os.Process.THREAD_PRIORITY_FOREGROUND); android.os.Process.setCanSelfBackground(false); Looper.prepareMainLooper(); Looper.getMainLooper().setSlowLogThresholdMs( SLOW_DISPATCH_THRESHOLD_MS, SLOW_DELIVERY_THRESHOLD_MS); // 4. 加载了`libandroid_servers.so` // Initialize native services. System.loadLibrary("android_servers"); ... // 5. 创建系统上下文 createSystemContext(); ... // 6. 创建SystemServiceManager:创建系统服务、管理服务生命周期,存取系统服务 // Create the system service manager. mSystemServiceManager = new SystemServiceManager(mSystemContext); mSystemServiceManager.setStartInfo(mRuntimeRestart, mRuntimeStartElapsedTime, mRuntimeStartUptime); LocalServices.addService(SystemServiceManager.class, mSystemServiceManager); // 为可以并行化的任务准备线程池 SystemServerInitThreadPool.get(); try { // 7. 启动核心关键服务 startBootstrapServices(); startCoreServices(); startOtherServices(); SystemServerInitThreadPool.shutdown(); } catch (Throwable ex) { ... } ... // 8. Looper.loop(); }
注释5创建系统上下文:
private void createSystemContext() { ActivityThread activityThread = ActivityThread.systemMain(); mSystemContext = activityThread.getSystemContext(); mSystemContext.setTheme(DEFAULT_SYSTEM_THEME); final Context systemUiContext = activityThread.getSystemUiContext(); systemUiContext.setTheme(DEFAULT_SYSTEM_THEME); }
得知
systemContext
是从ActivityThread#getSystemContext
;并设置了系统主题。重点关注注释7:启动系统服务,官方把系统服务分成三种类型引导服务、和兴服务、其他服务,这里简单列举一下:
大致统计了下
SystemServer
在启动过程中会启动96个左右的服务查看调用启动服务源码 e.g
SystemServer#startBootstrapServices
mDisplayManagerService = mSystemServiceManager.startService(DisplayManagerService.class);
启动服务是委托给
com.android.server.SystemServiceManager
public SystemService startService(String className) { ... // 1. 反射构造服务实例 Constructor<T> constructor = serviceClass.getConstructor(Context.class); service = constructor.newInstance(mContext); ... // Register it. //2. 添加到动态数组中 mServices.add(service); try { //3. 执行服务的onStart方法 service.onStart(); } catch (RuntimeException ex) { throw new RuntimeException("Failed to start service " + service.getClass().getName() + ": onStart threw an exception", ex); } }
以上代码表述了一个服务启动的过程。其中
SystemServerManager
用来管理(生死存亡、开启|关闭服务)系统各种服务,这对于后期学习系统C/S架构中的Binder机制通信有极大的作用。
Launcher启动过程过程
作为Android系统启动流程的最后一步:Home应用程序启动,
Home
也即Launcher
。应用程序在启动过程中会请求PMS
返回系统中已经安装的应用程序信息,并将这些信息转换成一个快捷启动图标显示在桌面上,这样用户就可以点击快捷图标启动程序了。源码地址
-
Launcher 程序是如何启动的?
由上一节得知SystemServer
进程会启动AMS
PMS
等服务。其中Launcher
程序是通过AMS
启动的
frameworks/base/services/java/com/android/server/SystemServer.java
private void startOtherServices() { mActivityManagerService.systemReady(() -> { ... // MakeXXXServiceReady } }
调用了
AMS
systemReady
方法... String mTopAction = Intent.ACTION_MAIN; ... public void systemReady(final Runnable goingCallback, TimingsTraceLog traceLog) { ... // Start up initial activity. mBooting = true; // Enable home activity for system user, so that the system can always boot. We don't // do this when the system user is not setup since the setup wizard should be the one // to handle home activity in this case. // 1. 启动HomeActivity startHomeActivityLocked(currentUserId, "systemReady"); } boolean startHomeActivityLocked(int userId, String reason){ if (mFactoryTest == FactoryTest.FACTORY_TEST_LOW_LEVEL && mTopAction == null) { // We are running in factory test mode, but unable to find // the factory test app, so just sit around displaying the // error message and don't try to start anything. return false; } // 2. 创建Home程序的启动Activity Intent Intent intent = getHomeIntent(); ActivityInfo aInfo = resolveActivityInfo(intent, STOCK_PM_FLAGS, userId); if (aInfo != null) { intent.setComponent(new ComponentName(aInfo.applicationInfo.packageName, aInfo.name)); // Don't do this if the home app is currently being // instrumented. aInfo = new ActivityInfo(aInfo); aInfo.applicationInfo = getAppInfoForUser(aInfo.applicationInfo, userId); ProcessRecord app = getProcessRecordLocked(aInfo.processName, aInfo.applicationInfo.uid, true); if (app == null || app.instr == null) { //3. 设置启动模式为FLAG_ACTIVITY_NEW_TASK intent.setFlags(intent.getFlags() | FLAG_ACTIVITY_NEW_TASK); final int resolvedUserId = UserHandle.getUserId(aInfo.applicationInfo.uid); // For ANR debugging to verify if the user activity is the one that actually // launched. final String myReason = reason + ":" + userId + ":" + resolvedUserId; // 4. 启动Activity mActivityStartController.startHomeActivity(intent, aInfo, myReason); } } else { Slog.wtf(TAG, "No home screen found for " + intent, new Throwable()); } return true; } Intent getHomeIntent() { Intent intent = new Intent(mTopAction, mTopData != null ? Uri.parse(mTopData) : null); intent.setComponent(mTopComponent); intent.addFlags(Intent.FLAG_DEBUG_TRIAGED_MISSING); if (mFactoryTest != FactoryTest.FACTORY_TEST_LOW_LEVEL) { // 2.1 设置Category为CATEGORY_HOME intent.addCategory(Intent.CATEGORY_HOME); } return intent; }
要启动的
action
为Intent.ACTION_MAIN
,category
为Intent.CATEGORY_HOME
。 Launcher应用程序Manifest
<application android:name="LauncherApplication" android:process="android.process.acore" android:label="@string/application_name" android:icon="@drawable/ic_launcher_home"> <activity android:name="Launcher" android:launchMode="singleTask" android:clearTaskOnLaunch="true" android:stateNotNeeded="true" android:theme="@style/Theme" android:screenOrientation="nosensor" android:windowSoftInputMode="stateUnspecified|adjustPan"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.HOME"/> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.MONKEY" /> </intent-filter> </activity> </application>
-
Launcher中应用图标启动分析
紧接着分析下点击应用图标到底发生了什么?
Launcher.java
public final class Launcher extends Activity implements View.OnClickListener, OnLongClickListener{ @Override protected void onCreate(Bundle savedInstanceState) { ... // 1. setContentView(R.layout.launcher); ... } }
launcher.xml
<com.android.launcher.DragLayer xmlns:android="http://schemas.android.com/apk/res/android" xmlns:launcher="http://schemas.android.com/apk/res/com.android.launcher" android:id="@+id/drag_layer" android:layout_width="fill_parent" android:layout_height="fill_parent"> <!-- The workspace contains 3 screens of cells --> <com.android.launcher.Workspace android:id="@+id/workspace" android:layout_width="fill_parent" android:layout_height="fill_parent" launcher:defaultScreen="1"> <include android:id="@+id/cell1" layout="@layout/workspace_screen" /> <include android:id="@+id/cell2" layout="@layout/workspace_screen" /> <include android:id="@+id/cell3" layout="@layout/workspace_screen" /> </com.android.launcher.Workspace> ... <com.android.launcher.DeleteZone android:id="@+id/delete_zone" android:layout_width="wrap_content" android:layout_height="49dip" android:scaleType="center" android:src="@drawable/ic_delete" android:background="@drawable/delete_zone_selector" android:layout_gravity="bottom|center_horizontal" android:visibility="invisible" launcher:direction="horizontal" /> </com.android.launcher.DragLayer>
重点关注
cell
,每一个cell
对应一个application
workspace_screen.xml
<com.android.launcher.CellLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:launcher="http://schemas.android.com/apk/res/com.android.launcher" android:layout_width="fill_parent" android:layout_height="fill_parent" launcher:cellWidth="80dip" launcher:cellHeight="100dip" launcher:longAxisStartPadding="0dip" launcher:longAxisEndPadding="55dip" launcher:shortAxisStartPadding="0dip" launcher:shortAxisEndPadding="0dip" launcher:shortAxisCells="4" launcher:longAxisCells="4" />
CellLayout
是一个自定义View,继承ViewGroup
,父控件为Workspace
。回到Launcher.java
:private static class DesktopBinder extends Handler implements MessageQueue.IdleHandler{ ... @Override public void handleMessage(Message msg) { Launcher launcher = mLauncher.get(); if (launcher == null || mTerminate) { return; } switch (msg.what) { case MESSAGE_BIND_ITEMS: { // 1.执行了bindAppWidgets launcher.bindItems(this, mShortcuts, msg.arg1, msg.arg2); break; } case MESSAGE_BIND_DRAWER: { launcher.bindDrawer(this, mDrawerAdapter); break; } case MESSAGE_BIND_APPWIDGETS: { launcher.bindAppWidgets(this, mAppWidgets); break; } } } } private void bindItems(Launcher.DesktopBinder binder, ArrayList<ItemInfo> shortcuts, int start, int count){ final Workspace workspace = mWorkspace; ... for ( ; i < end; i++) { final ItemInfo item = shortcuts.get(i); switch (item.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: // 2. 创建快捷方式View 并添加到 workspace子控件的cellLayout中 final View shortcut = createShortcut((ApplicationInfo) item); workspace.addInScreen(shortcut, item.screen, item.cellX, item.cellY, 1, 1, !desktopLocked); break; } } }
当从
PMS
获取到已经安装的应用程序后,将这些appinfo
绑定到Launcher
的界面中。注释2调用createShortcut
View createShortcut(int layoutResId, ViewGroup parent, ApplicationInfo info) { TextView favorite = (TextView) mInflater.inflate(layoutResId, parent, false); if (!info.filtered) { info.icon = Utilities.createIconThumbnail(info.icon, this); info.filtered = true; } favorite.setCompoundDrawablesWithIntrinsicBounds(null, info.icon, null, null); favorite.setText(info.title); favorite.setTag(info); // 1. favorite.setOnClickListener(this); return favorite; }
注释1 很关键,快捷方式的点击事件委托给
Launcher
Activity ,接着看Launcher#onClick
方法/** * Launches the intent referred by the clicked shortcut. * * @param v The view representing the clicked shortcut. */ public void onClick(View v) { Object tag = v.getTag(); if (tag instanceof ApplicationInfo) { // Open shortcut final Intent intent = ((ApplicationInfo) tag).intent; //1. startActivitySafely(intent); } else if (tag instanceof FolderInfo) { handleFolderClick((FolderInfo) tag); } } void startActivitySafely(Intent intent) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { // 2. startActivity(intent); } catch (ActivityNotFoundException e) { Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); } catch (SecurityException e) { ... } }
注释1 & 2 调用了
startActivitySafely
也即调用了startActivity
, 这样目标程序就会被启动。
Google Architecture ViewModel
关于ViewModel 介绍,文章不过多阐述。官方文档
本文将从三个方面做一定阐述
为什么需要ViewModel
-
你需要处理配置变化
- 笔者认为,用户可以随时改变配置(i.e 旋转屏幕,切换语言,切换系统文字大小 ...),这可能会导致当前Activity重建,这些都不受开发者的控制,但是你又不得不处理它
- 可能很多APP在配置清单文件中申明了每一个Activity
orientation = portrait
,但是你无法禁止用户去改变语言、文字大小。这样就可能会导致Activity被移除或者重新创建
-
为什么
onSaveInstanceState
依旧不够传统的做法都是在配置发生变化即
onSaveInstanceState
方法去save data, 在onCreate去restore data但是这里有两个限制
onSaveInstanceState
方法不能够缓存较大的数据,笔者之前尝试缓存上百兆数据发现抛出了TransactionTooLargeException
- 保存的数据一定需要实现
serializable
或者Parceable
, 但是有时候这些数据来自第三方库,我们不能修改它,对于某些场景,很难在onSaveonSaveInstanceState
中保存数据
基于上述两点,ViewModel应运而生
- 配置改变前后数据存储与恢复
一些例子
基础功能
public class ZeroViewModel extends ViewModel {
public User user;
}
public class ZeroDemo extends AppCompatActivity {
private TextView tv;
private ZeroViewModel vm;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tv_btn);
tv = findViewById(R.id.tv_simple);
vm = ViewModelProviders.of(this).get(ZeroViewModel.class);
System.out.println("szw vm.user = " + vm.user);
}
// android:onClick="onClickSimpleButton"
public void onClickSimpleButton(View v) {
vm.user = new User(23, "jorden");
}
}
旋转屏幕,vm.user 依旧 != null
同一个Activity不同实例
- 同时存在两个实例
public class SameClass01 extends AppCompatActivity {
private TextView tv;
private ZeroViewModel vm;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tv_btn);
tv = findViewById(R.id.tv_simple);
vm = ViewModelProviders.of(this).get(ZeroViewModel.class);
System.out.println("szw SameClass01 : " + vm.user);
}
// launch the second instance
// android:onClick="onClickSimpleButton"
public void onClickSimpleButton(View v) {
vm.user = new User(100, "SuperMario");
startActivity(new Intent(this, SameClass01.class));
}
}
即使有两个同类的Activity实例,第一个vm.user 持有的依旧是Mario,第二个vm.user 持有null
- finish再重新创建
public class SameClass02 extends AppCompatActivity {
private TextView tv;
private ZeroViewModel vm;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tv_btn);
tv = findViewById(R.id.tv_simple);
vm = ViewModelProviders.of(this).get(ZeroViewModel.class);
System.out.println("szw SameClass02 onCreate() : " + vm.user);
}
// android:onClick="onClickSimpleButton"
public void onClickSimpleButton(View v) {
vm.user = new User(22, "test");
}
// android:onClick="onClickSimpleButton2"
public void onClickSimpleButton2(View v) {
System.out.println("szw SameClass02 : saved = "+vm.user);
}
}
先启动SameClass02 ,执行onClickSimpleButton,finish重新打开,日志输出null
上述两个例子表现正常
和Static申明的变量比较
- 基础比较
public class SameVm {
public static User user;
}
public class ZeroDemo extends AppCompatActivity {
private TextView tv;
private ZeroViewModel vm;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tv_btn);
tv = findViewById(R.id.tv_simple);
String value = savedInstanceState == null ? "emptyBundle" : savedInstanceState.getString("key");
System.out.println("szw onCreate() " + value);
vm = ViewModelProviders.of(this).get(ZeroViewModel.class);
System.out.println("szw vm.user = " + vm.user);
System.out.println("szw static = "+SameVm.user);
}
// android:onClick="onClickSimpleButton"
public void onClickSimpleButton(View v) {
vm.user = new User(23, "jorden");
SameVm.user = new User(21, "king");
}
}
旋转屏幕后,日志输出
szw vm.user = User{id=23, name='jorden'}
szw static = User{id=21, name='king’}
- 终止应用
Terminate Application
和1同样的操作
szw vm.user = null
szw static = null
从上面两个例子,感觉没什么不同。他们都能缓存数据,终止应用程序都会被销毁
它们的不同之处:
- ViewModel 主要是为了解耦,有点类似于MVP中的P,ViewModel 是MvvM中VM。你能在ViewModel中做异步操作(i.e访问网络),你可以改变data并且让View接受到通知LiveData
- static value 能被任何类修改,但是ViewModel 是Activity的私有变量,有点类似ThreadLocal
- ViewModel 可以判断Activity是正常销毁或者配置改变,进而做出不同的响应,finish->removedata ,configurationchange->savedata,静态变量却不能
相关注意事项
- ViewModel不要应用Activity等相关实例,容易造成内存泄漏
- 如果你需要在ViewMolde中获取Resource LocationManager等系统服务,可以继承AndroidViewModel
- ViewModel本身不支持事件模型(EventBus),你可以使用LiveData,当然为了解决旋转屏幕后,再次注册Observer,重复提示,可以使用SingleLiveEvent
public class DupliViewModel extends ViewModel {
private SingleLiveEvent<String> message = new SingleLiveEvent<>();
public void fetchMessage(){
message.setValue("A New Value");
}
public LiveData<String> getMessage() {
return message;
}
}
public class DupliObserverDemo extends AppCompatActivity {
private TextView tv;
private DupliObserverDemo self;
private DupliViewModel vm;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tv_btn);
self = this;
tv = findViewById(R.id.tv_simple);
vm = ViewModelProviders.of(this).get(DupliViewModel.class);
vm.getMessage().observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable String s) {
System.out.println("szw updated ~");
Toast.makeText(self, "updated "+s, Toast.LENGTH_SHORT).show();
}
});
}
// android:onClick="onClickSimpleButton"
public void onClickSimpleButton(View v) {
vm.fetchMessage();
}
}
自定义Gradle-Plugin 插件(一)
自定义Gradle-Plugin 插件(一)
官方文档给出了详细的实现步骤,笔者
将参考官方文档做一些基础介绍,额外增加一个实例:通过自定义插件修改编译后的class文件,本文按照以下三个方面进行讲解
- 插件基础介绍
- 三种插件的打包方式
- 实例Demo&Debug调试
插件基础介绍
根据插件官方文档定义,插件打包了可重用的构建逻辑,可以适用不同的项目和构建。
Gradle 提供了很多官方插件,用于支持Java、Groovy等工程的构建和打包。同时也提供了自定义插件机制,让每个人都可以通过插件来实现特定的构建逻辑,并可以把这些逻辑打包起来,分享给其他人。
插件的源码可以是用Groovy、Scale、Java三种语言,笔者对Scale不熟悉,对Groovy也略知一二。Groovy用于实现构建生命周期(如Task的依赖)有关逻辑,Java用于实现核心逻辑,表现为Groovy调用Java代码
另外,还有很多项目使用Eclipse 或者Maven进行开发构建,用Java实现核心业务代码,将有利于实现快速迁移。
三种插件的实现方式
笔者编写自定义插件相关代码时,对很多GradlePluginForAndroid
相关api 不熟悉,例如Transform
、TransformOutputProvider
等,没关系,官方文档gradle-plugin-android-api 将会是你最好的学习教程
Build Script
把插件写在build.gradle 文件中,一般用于简单的逻辑,只在改build.gradle 文件中可见,笔者常用来做原型调试。在我们指定的module build.gradle 中:
/**
* 分别定义Extension1 和 Extension2 类,申明参数传递变量
*/
class Extension1 {
String testVariable1 = null
}
class Extension2 {
String testVariable2 = null
}
/**
* 插件入口类
*/
class TestPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
//利用Extension创建e1 e2 闭包,用于接受外部传递的参数值
project.extensions.create('e1', Extension1)
project.extensions.create('e2', Extension2)
//创建readExtension task 执行该task 进行参数值的读取以及自定义逻辑...
project.task('readExtension') << {
println 'e1 = ' + project['e1'].testVariable1
println 'e2 = ' + project['e2'].testVariable2
}
}
}
/**
* 依赖我们刚刚自定义的TestPlugin,注意 使用e1 {} || e2{} 一定要放在apply plugin:TestPlugin 后面, 因为 app plugin:TestPlugin
* 会执行 Plugin的apply 方法,进而利用Extension 将e1 、e2 和 Extension1 Extension2 绑定,编译器才不会报错
*/
apply plugin: TestPlugin
e1 {
testVariable1 = 'testVariable1'
}
e2 {
testVariable2 = 'testVariable2'
}
相关注释说明已经在代码中简单说明,如果读者依然不熟悉或者想了解更多内容,可以在api文档中进行查阅。
然后执行readExtension
task 即可
./gradlew -p moduledir readExtension --stacktrace
buildSrc 项目
将插件源代码放在rootProjectDir/buildScr/scr/main/groovy
中,只对该项目中可见,适用于逻辑较为复杂,但又不需要外部可见的插件,本文不介绍,有兴趣可以参考此处
独立项目
一个独立的Groovy 和Java项目,可以把这个项目打包成jar文件包,一个jar文件包还可以包含多个插件入口,可以将文件包发布到托管平台上,共其他人使用。
其实,IntelliJIEDA 开发插件要比Android Studio要方便一点,因为有对应的Groovy module模板,但如果我们了解IDEA项目文件结构,就不会受到这个局限,无非就是一个build.gradle 构建文件夹scr源码文件夹
-
在Android Studio中新建
Java Library
moduleuploader
(moduleName 不重要,根据实际情况定义) -
修改项目文件夹
- 移除java文件夹,因为在这个项目中用不到java代码
- 添加Groovy文件夹,主要的代码文件放在这里
- 添加resource文件夹,存放用于标识gradle插件的meta-data
-
修改build.gradle 文件
//removed java plugin apply plugin: 'groovy' apply plugin: 'maven' repositories { mavenCentral() } dependencies { compile gradleApi()//gradle sdk compile localGroovy()//groovy sdk compile fileTree(dir: 'libs', include: ['*.jar'])
}
uploadArchives {
repositories {
mavenDeployer {
//设置插件的GAV参数
pom.groupId = 'cn.andaction.plugin'
pom.version = '1.0.0'
//文件发布到下面目录
repository(url: uri('../repo'))
}
}
}
```
-
建立对应文件
├── build.gradle
├── libs
├── plugin.iml
└── src
└── main
├── groovy
│ └── cn
│ └── andaction
│ └── uploader
│ ├── XXXPlugin.groovy
│ └── YYYY.groovy
└── resources
└── META-INF
└── gradle-plugins
└── uploader.properties
* Groovy文件夹中的类,一定要修改成``.groovy`` 后缀,IDE才会正常识别
* resource/META-INF/gradle-plugins这个文件夹结构是强制要求的,否则不能识别成插件
另外,关于uploader.properties ,写过java的同学应该知道,这是一个java的properties文件,是``key=value``的格式,这个文件内容如下
```properties
implementation-class=cn.andaction.uploader.XXXPlugin.groovy
```
用于**指定插件入口类,其中apply plugin: '${当前配置文件名}**
### 实例Demo
> 自定义``gradle-plugin`` 并利用[javassist](https://github.com/jboss-javassist/javassist) 类库工具修改指定编译后的class文件
笔者参考了[通过自定义Gradle插件修改编译后的class文件](https://www.jianshu.com/p/417589a561da)
预备知识
- [Transform API](http://google.github.io/android-gradle-dsl/javadoc/current/)
- [javassist](https://github.com/jboss-javassist/javassist)
1. buid.gradle 增加类库依赖
```gradle
compile 'com.android.tools.build:gradle:3.0.1'
compile group: 'org.javassist', name: 'javassist', version: '3.22.0-GA'
```
2. 自定义Transform
```java
public class PreDexTransform extends Transform {
private Project project
/**
* 构造函数 我们将Project 保存下来备用
* @param project
*/
PreDexTransform(Project project) {
this.project = project
}
....
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
//transformInvocation.inputs 有两种类型,一种是目录,一种是jar包 分开对其进行遍历
transformInvocation.inputs.each { TransformInput input ->
// 对类型为文件夹 的input进行遍历 :对应的class字节码文件
// 借用JavaSsist 对文件夹的class 字节码 进行修改
input.directoryInputs.each { DirectoryInput directoryInput ->
TestInject.injectDir(directoryInput.file.absolutePath, 'cn.andaction.plugin')
File des = transformInvocation.getOutputProvider().getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, des)
}
// 对类型为jar的input进行遍历 : 对应三方库等
input.jarInputs.each { JarInput jarInput ->
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith('.jar')) {
jarName = jarName.substring(0, jarName.length() - 4) // '.jar'.length == 4
}
File dest = transformInvocation.getOutputProvider().getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
// 将输入内容复制到输出
FileUtils.copyFile(jarInput.file, dest)
}
}
super.transform(transformInvocation)
}
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
super.transform(context, inputs, referencedInputs, outputProvider, isIncremental)
}
}
```
3. 对directoryInputs 文件夹下的class文件遍历,找到符合需要的.class 文件,通过javassit 类库对字节码文件进行修改
``TestInject.groovy``
```java
File dir = new File(path)
classPool.appendClassPath(path)
if (dir.isDirectory()) {
dir.eachFileRecurse { File file ->
String filePath = file.path
// 这里我们指定修改TestInjectModel.class字节码,在构造函数中增加一行i will inject
if (filePath.endsWith('.class')
&& filePath.endsWith('TestInjectModel.class')) {
// 判断当前目录是否在我们的应用包里面
int index = filePath.indexOf(packageName.replace('.',File.separator))
if (index != -1) {
int end = filePath.length() - 6 // '.class'.length = 6
String className = filePath.substring(index, end)
.replace('\\', '.')
.replace('/', '.')
// 开始修改class文件
CtClass ctClass = classPool.getCtClass(className)
// 拿到CtClass后可以对 class 做修改操作(addField addMethod ..)
if (ctClass.isFrozen()) {
ctClass.defrost()
}
CtConstructor[] constructors = ctClass.getDeclaredConstructors()
if (null == constructors || constructors.length == 0) {
// 手动创建一个构造函数
CtConstructor constructor = new CtConstructor(new CtClass[0], ctClass)
constructor.insertBeforeBody(injectStr)
//constructor.insertBefore() 会增加super(),且插入的代码在super()前面 ctClass.addConstructor(constructor)
} else {
constructors[0].insertBeforeBody(injectStr)
}
ctClass.writeFile(path)
ctClass.detach()
}
}
}
}
```
4. 发布插件代码到本地
```gradle
./gradlew -p moduleDir/ clean build uploadArchives -stacktrace
```
5. 运行测试
- build.gradle
```gradle
repositories {
maven {
url 'file:your-project-dir/repo/'
}
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'cn.andaction.plugin:uploader:1.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
```
```gradle
apply plugin: 'uploader'
```
- 修改代码
1. 新增``TestInjectModel.java``,空实现
2. app入口类``onCreate``方法调用``new TestInjectModle()``
- 执行``make project``
![](http://o72lx27yo.bkt.clouddn.com/plugin-project-demo-result.png)
6. 插件调试
参考[Android Studio 调试Gradle-plugin](http://blog.csdn.net/ceabie/article/details/55271161)
**注意,在修改插件源码后,需要重新执行``uploadArchives`` 发布插件代码,新增/修改的代码断点才能起作用**
Git 工作日志
Android彻底组件化方案实践
思维方式
gradle 构建常用命令记录
gradle build --refresh-dependencies
编译时强制刷新依赖
gradle build --offline
编译时使用缓存
configurations.all {
//每隔24小时检查远程依赖是否存在更新
resolutionStrategy.cacheChangingModulesFor 24, 'hours'
//每隔10分钟..
//resolutionStrategy.cacheChangingModulesFor 10, 'minutes'
// 采用动态版本声明的依赖缓存10分钟
resolutionStrategy.cacheDynamicVersionsFor 10*60, 'seconds'
}
dependencies {
// 添加changing: true
compile group: "group", name: "module", version: "1.1-SNAPSHOT", changing: true
//简写方式
//compile('group:module:1.1-SNAPSHOT') { changing = true }
}
获取Java泛型T 的T.class
API了解
-
getGenericInterfaces()
- 表示当前Class 所表示的实体。如果这个直接超类是参数化类型的,则返回的Type对象必须明确反映在源代码中声明时使用的类型 例如
public class GT1 extends GT2<Integer>{ public static void main(String[] args) { System.out.println(((ParameterizedType)new GT1().getClass().getGenericSuperclass())); } }
则输出结果即为:
xxx.GT2<java.lang.Integer>
如果此Class代表的是Object 类、接口、基本类型或 void,则返回 null。。如果此对象表示一个数组类,则返回表示 Object 类的 Class 对象
-
getGenericInterfaces()
- 获取当前实现接口,返回Type[]
实现方案
/**
* 获取泛型T 的T.class
*
* @param object 当前泛型申明所在对象
* @param genericIndex 可能存在多个泛型 获取指定泛型下标
* @return
*/
public static <T> Class<T> getGenericClazz(Object object, int genericIndex) {
Class targetClazz = null;
try {
Class currentClass = object.getClass();
Type genType = currentClass.getGenericSuperclass();
Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
Type typeClass = params[genericIndex];
if (typeClass instanceof Class) {
targetClazz = (Class) typeClass;
} else {
throw new RuntimeException("Whether child class declare generic type");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
return targetClazz;
}
}
这里说明一点:在测试的此方法的时候,实例化 在申明泛型的类的子类
,然后再调用此方法
项目流程
常见错误记录
1 . mysql安装成功且配置好环境变量后 出现:Can't connect to local MySQL server through socket '/tmp/mysql.sock'
网上很多解决方案。发现mysql service 未启动
sudo /usr/local/mysql/support-files/mysql.server restart
另外启动和停止分别是:
sudo /usr/local/mysql/support-files/mysql.server start
sudo /usr/local/mysql/support-files/mysql.server stop
- 密码未授权
Android Binder跨进程通信
Android 重签名
jarsigner -verbose -keystore debug.keystore -storepass android -keypass android -signedjar Thinkdrive_signed.apk Thinkdrive_temp.apk androiddebugkey
第一格表头 | 第二格表头 |
---|---|
jarsigner | 是Java的签名工具 |
verbose | 显示出签名详细信息 |
keystore | 使用当前目录中的debug.keystore签名证书文件。 |
storepass | 密钥口令 |
signedjar ThinkDrive_signed.apk | 表示签名后生成的APK名称, |
ThinkDrive_temp.apk | 表示未签名的APK, |
androiddebugkey | debug.keystore的别名 |
自动刷新token方案
本文结合
Rxjava
Okhttp
Retrofit
开源方案予以实现
制定目标
- 执行业务请求时,
accessToken
失效,自动执行refreshToken
,携带最新accessToken
重试之前的业务请求 - 多业务请求并发访问时,所有请求均失效,保证仅有一次
refreshToken
操作 - 对
refreshToken
进行合理的节流 - 业务请求+
refreshToken
合理的降级策略 - 特殊场景:
NoRefreshToken
白名单策略
落地实现
-
执行业务请求时,
accessToken
失效,自动执行refreshToken
,携带最新accessToken
重试之前的业务请求- 设计模式之全局动态代理模式
Rxjava
retryWhen
操作符
Class<TestApi> aClass = TestApi.class; testApi = retrofit.create(aClass); TokenRefreshProxy proxy = new TokenRefreshProxy(testApi); testApi = (TestApi) Proxy.newProxyInstance(aClass.getClassLoader(), new Class[]{aClass}, proxy);
动态代理以保证每一个接口执行时都会回调
TokenRefreshProxy#invoke
,在这个方法中控制刷新token刷新时机@Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { return Observable.defer(new Callable<ObservableSource<TokenWrapper>>() { @Override public ObservableSource<TokenWrapper> call() throws Exception { // 请求接口时携带的参数中的token 和全局token若不一致 就进行替换 String declareToken = getDeclareToken(method, args); // 获取请求参数中的token String globalToken = MyApp.sToken; if (!TextUtils.isEmpty(globalToken) && !TextUtils.equals(declareToken, globalToken)) { return Observable.just(new TokenWrapper(true, globalToken)); } else { return Observable.just(new TokenWrapper(false, null)); } } }).flatMap(new Function<TokenWrapper, ObservableSource<?>>() { @Override public ObservableSource<?> apply(TokenWrapper wrapper) throws Exception { if (wrapper.isNeedRefresh) { // 替换请求参数为最新的token updateMethodToken(method, args, wrapper.newestToken); } // 执行业务请求 ObservableSource<?> observableSource = (ObservableSource<?>) method.invoke(proxyObj, args); return observableSource; } }).retryWhen(new Function<Observable<Throwable>, ObservableSource<?>>() { @Override public ObservableSource<?> apply(Observable<Throwable> throwableObservable) throws Exception { return throwableObservable.flatMap(new Function<Throwable, ObservableSource<?>>() { @Override public ObservableSource<?> apply(Throwable throwable) throws Exception { if(throwable instanceof TokenInvalidException){ // 刷新token return RefreshTokenLoader.getInstance().getTokenLocked(); } // 其他异常直接向上抛出 return Observable.error(throwable); }); } }); }
-
多业务请求并发访问时,所有请求均失效,保证仅有一次
refreshToken
操作- 多线程并发访问同步控制
PublishSubject
操作符, 四种Subject介绍
private class RefreshTokenLoader{ // 当前refreshToken操作状态 // 原子性 Atomic**, 不可分割操作 // volatile 保证线程可见性 private volatile AtomicBoolean refreshTokenFlag = new AtmoicBoolean(false) private PublishSubject<?> publishSubject; // 单例 private RefreshTokenLoader() { } public static RefreshTokenLoader getInstance() { return Holder.sInstance; } public static class Holder { private static final RefreshTokenLoader sInstance = new RefreshTokenLoader(); } public Observable<?> getTokenLocked() { if (refreshTokenFlag.compareAndSet(false, true)) { // 传统做法是对boolean 做判断 + 复制 ,本身就是一个非原子操作,所以这里选择AtomicBoolean API // 每次token失效时,都需要重新实例化一个publishSubject publishSubject = PublishSubject.create(); Observable<?> refreshTokenObservable = createRefreshTokenObservable.doOnNext(tokenEntity -> { // .... 缓存最新的accessToken refreshTokenFlag.set(false); }).doOnError(throwable -> { // ... 跳转到登陆页面 refreshTokenFlag.set(false) }).subscribeOn(Schedulers.io()); refreshTokenObservable.subscribe(publishSubject); } else { // get token locked } return publishSubject; } }
-
对
refreshToken
进行合理的节流
某种场景:A,B 请求,A请求耗时10s, B请求耗时5s, refreshToken 耗时2s, A,B请求同时并发,B先感知到token失效,并完成换取token工作,此时A请求才感知到token失效,假设token有效期为 > 3s ,那么A请求就没有必要再去换取token了,直接复用B请求换取的token进行重试RefreshTokenLoader.java public Observable<?> getTokenLocked(){ //..... Observable<?> refreshTokenObservable = createRefreshTokenObservable.doOnNext(tokenEntity -> { // .... 缓存最新的accessToken // saveRefreshTokenTime 此处记录当前时间为t1 sp.edit().putLong("t1", System.currentTimeMillis()); }) // .... } TokenRefreshProxy.java xxx.retryWhen(new Function<Observable<Throwable>, ObservableSource<?>>() { @Override public ObservableSource<?> apply(Observable<Throwable> throwableObservable) throws Exception { return throwableObservable.flatMap(new Function<Throwable, ObservableSource<?>>() { @Override public ObservableSource<?> apply(Throwable throwable) throws Exception { if(throwable instanceof TokenInvalidException){ long currentTime = System.currentTimeMillis(); long t1 = sp.getLong("t1"); // 规定30s之内token均有效 ,直接复用上次获取的token即可 if (t1 != 0l && currentTime - t1 <= 30 * 1000 && !TextUtils.isEmpty(globalToken)) { return Observable.just(globalToken); } // 刷新token return RefreshTokenLoader.getInstance().getTokenLocked(); } // 其他异常直接向上抛出 return Observable.error(throwable); }); } });
这里主要目的是多请求并发访问情况下为了减少不必要额外
refreshToken
操作。当然你也可以将这个30s 设置为token有效期。 -
业务请求+
refreshToken
合理的降级策略每一个请求重试次数不能超过指定次数
TokenRefreshProxy.java int maxRetryCount = 3; XXX.retryWhen(throwableObservable -> { return throwableObservable.zipWith(Observable.range(1, maxRetryCount), (BiFunction<Throwable, Integer, ThrowableWrapper>) (throwable, integer) -> { if (throwableObservable instanceof TokenInvalidException) { if (topActivityIsLogin()) { return new ThrowableWrapper(throwable, maxRetryCount); } // token失效正常重试 return new ThrowableWrapper(throwable, integer); } return new ThrowableWrapper(throwable, integer); }).concatMap(throwableWrapper -> { final int retryCount = throwableWrapper.getRetryCount(); if (retryCount >= maxRetryCount) { // 重试次数用完了 || 当前已经跳转到登陆页了 || 其他非token过期异常 直接向上抛 return Observable.error(throwableWrapper.getSourceThrowable()); } long currentTime = System.currentTimeMillis(); long t1 = sp.getLong("t1"); // 规定30s之内token均有效 ,直接复用上次获取的token即可 if (t1 != 0l && currentTime - t1 <= 30 * 1000 && !TextUtils.isEmpty(globalToken)) { return Observable.just(globalToken); } // 刷新token return RefreshTokenLoader.getInstance().getTokenLocked(); }) })
-
特殊场景:
NoRefreshToken
白名单策略
某些接口不需要这套token验证机制- 运行时注解定义及其获取
@Documented @Target(METHOD) @Retention(RUNTIME) public @interface NoTokenProxy { } TokenRefreshProxy.java @Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { Annotation[] declaredAnnotations = method.getDeclaredAnnotations(); for (int i = 0; i < declaredAnnotations.length; i++) { Annotation annotation = declaredAnnotations[i]; if (annotation instanceof NoTokenProxy) { // 说明不需要加token 重试代理 直接执行实际函数并返回 return method.invoke(proxyObj, args); } } ///... 刷新token机制代码 }
初探React-Native
React-Native
搭建开发环境
安装
-
Node.js react-native-cli 安装,npm 设置淘宝开源镜像:
npm config set registry https:// registry.npm.taobao.org --global ,
npm config set disturl https://npm.taobao.org/dist --global安装react-native-cli,npm install -g yarn react-native-cli 其中yarn是facebook提供替代npm的工具 ,可以加速node模块
-
android studio SDK Android开发环境搭建,设置环境变量ANDROID_HOME,校验是否设置成功:echo $ANDROID_HOME
推荐安装的工具
-
Watchman 是一个提供监视文件系统变更的工具。安装此工具可以提高开发时的性能,packager可以快速捕获文件的变化而实现实时刷新
-
Flow 是一个静态的JS类型检查工具,这个是facebook自家代码的规范,这一语法并不属于ES标准
-
Nuclide facebook提供的基于atom的集成开发环境,可用于编写、运行、调试react-native应用
测试安装
react-native init Project
cd Project
react-native run-android
遇到的坑
- react-native init Project 卡住,使用淘宝npm 镜像源,或者直接去github 上down代码
- react-native run-android 卡住 : gradle 版本不一致导致,更改/android/gradle/wrappwe/gradle-wrapper.properties 中distributionUrl gradle 版本 与当前版本一致即可,如果当前提示gradle 版本必须是v1,且v1 > 当前gradle version, 修改工程根目录下的build.gradle classpath 为合适的版本即可
运行官方 Example 例子
方式一(Android):
-
github上clone https://github.com/facebook/react-native.git
-
找到Example中 UIExplorer
-
搭建NDK环境 ndk 编译版本需要根据官方指定prerequisites
-
切换到我们刚刚clone的项目目录下
./gradlew :Examples:UIExplorer:android:app:installDebug<br> ./packager/packager.sh
最好在执行 ./gradlew :Examples:UIExplorer:android:app:installDebug 先执行 npm install 安装我们项目所依赖的一些包
入坑
在编译demo项目是出现
Cause: ndk-build binary cannot be found, check if you've set $ANDROID_NDK environment variable correctly or if ndk.dir is setup in local.properties
Android sdk 以及ndk 环境都配置好了,并且local.properties也在项目的根目录下指定了,
bash_profile need declare ANDROID_NDK ANDROID_HOME
一定要注意 fish 和 zsh fish是 set -x PATH /XXX 详情参考fishshell而 zsh 是 export PATH=${PAHT}: ....
方式二(Android ):
方式一需要一些环境支持(ndk),而且版本必须一致,
- 将UIExplorer中的js 复制到我们的项目中去,并且拷贝核心java文件
- 修改项目中的app build.gradle 相关sdk 版本 尽可能与 UIExplorer 中的版本保持一致
- 修改UIExplorerApplication中修改加载js的一个配置方法,修改为我们项目的js路径
方式三(IOS)
ios 只需要运行XXX.xcodeproj文件 点击运行即可i
React Native组件
- 概念
React 组件让你将UI分割成独立的、可重用的一些碎片部分,这些都是互相独立的 ,是对View的一个抽象 - 创建组件的三种方式
2.1 ES6创建组件
2.2 ES5创建组件
2.3 函数式定义无状态组件
React Native组件生命周期
-
什么是组件的生命周期
-
组件的生命周期中都包括哪些方法,各有什么作用
-
生命周期的方法都在什么时候调用
React Native 导入和导出
-
如何导出一个组件,如何使用导出的组件
ES6 :export default class XXX ,setup.js import XXX from './XXX.js'
ES5 : var XXX = React.createClass({}) module.exports = XXX, 使用方式同ES6 -
如何导出一个变量和常量,如何使用导出的变量和常量
import EIComponent, {variableA, VariableB} from './EIComponent';
-
如何导出一个方法,如何使用导出的方法
import EIComponent, {function} from './EIComponent';
React Native props使用详解
-
什么是props ?
组件的属性,一般情况下都是使用组件方向组件传递相关属性,但是组件可以申明自己默认的属性 只读的
-
如何使用props ?
setup.js <Component name='XXX' /> Component.js {this.props.name}
-
什么是默认属性以及它的作用
如果调用方未传递属性给组件,组件可以默认申明属性,作用:自定义组件默认值
-
如何对props 进行约束和检查
static propTypes={ name:PropTypes.string //name必须是字符串 sex:PrpTypes.string.isRequired //sex 必须要传入 // PropTypes 需要从react 组件导入 import React, {Component, PropTypes} from 'react' }
-
props使用小技巧之延展操作符
如果组件需要传递多个参数
setup.js
var params = {name:'testname',age:30,sex:'男'}; <Component {...params} />
-
props 使用小技巧之解构赋值
setup.js
var params = {name:'testname',age:30,sex:'男'}; // 组件仅仅需要其中的name 、sex 属性 var {name,sex} = params; <Component name={name} sex={sex} />
React-Native State 使用
-
什么是state ?
可以理解为一个状态机, 维护当前状态,当state发生变化的时候更新dom
-
如何使用state ?
define: state = { size:80 } usage: this.state.size
-
state 用处 ?
通过动态改变state状态 来达到更新当前组件
React-Native ref
tips:
组件并不是真正的Dom节点,而是存储于内存当前的一种数据结构,称之为虚拟结构,
变动的虚拟dom转换为真实dom 这个过程叫做Dom def(有点类似增量编译)
-
什么是ref ?
是组件的一种特殊属性, 可以理解为组件被渲染后,指向组件的一个引用,通过组件的引用来获取真实的组件
-
使用ref
定义组件ref两种方式:
1.<Component
ref="refalias"
/>
usage:
var obj = this.refs['refalias'] or this.refs.refalias
2. <Component
ref={refalias=>this.refalias=refalias}
/>
usage:
var obj = this.refalias
React-Native ES6 VS ES5
ES6 介绍
es6 全称ECMAScript 6.0 ECMAScript 是ECMA 制定的标准化脚本语言,目前JavaScript 使用的是ECMAScript 版本为ECMAScript-262
六大方面特性
- 类(Class)
class TestClass {
// 构造方法,实例化的时候将会被调用,如果不指定,默认会有一个不带参数的默认构造函数
constructor(var1,var2){
this.var1 = var1;
this.var2 = var2;
}
// toString 是原型对象上的属性
toString(){
console.log('var1:' + this.var1 + ',var2 :' + this.var2);
}
}
usage
var obj = new TestClass('test1','test2');
obj.toString();
console.log(obj.hasOwnProperty('var1'));// true
console.log(obj.hasOwnProperty('toString'))// false
console.log(obj._proto_.hasOwnProperty('toString')) // true
class ChildTestClass extends TestClass{
consructor(action){
// 自雷必须要在constructor方法中制定super方法,否则在新建实例的时候会报错
super('var1','var2');
this.action = action;
}
}
//....
- 模块(Module)
ES5 不支持原生的模块化,在ES6 中,模块将作为重要的组成部分被添加进来。模块的功能主要由export 和import 组成。每一个模块都有自己单独的作用域,模块之间的相互调用关系是通过export 来规定模块对外暴露的接口,通过import 来引用其他模块提供的接口。同时还为模块创造了命名空间,防止函数命名冲突
export var variableA = 'Rainbow';
export const sqrt = Math.sqrt;// 导出常量
var variableX = 'a';
var variableY = 'b';
export {variableX,variableY};
export function myFunction(params){
///....imp
}
import {Component} from 'Component' Component.js
import {variableX,variableY} from 'test' test.js
一条import 语句可以同时导入默认方法和其他变量:
import defaultMethod ,{otherMethod} from 'XXX.js';
- 箭头函数
这是ES6 中最令人激动特性之一。=>不止是关键字function的简写,它还带来了其他好处,箭头函数与包围它的代码共享同一个this,能帮你很好解决this指向问题, 例如:var self = this,or var that = this 这种引用外围this的模式。但借助=> 就不需要这种模式了
class PauseMenu extends React.Component{
componentWillMount({
AppStateIOS.addEventListener('change',this.onAppPaused);
}
componentWillUnmount(){
AppStateIOS.removeEventListener ('change',this.onAppPaused);
}
onAppPaused= (event)=>{
// 把方法直接作为一个arrow function的属性来定义,初始化的时候绑定好this指针
}
}
- ES6 不再支持Mixins
在ES5 下,我们经常使用mixin来为组件添加一些新的方法,ES6使用增强组件来替代Mixins,网上还有其他方案例如:react-mixin
- ES6不再有自动绑定
// 通过使用bind() 来绑定'this'
<div onClick={this.tick.bind(this)}></div>
// 也可通过使用箭头函数来实现
<div onClick={()=>this.tick()}>
无论是箭头函数还是bind() 每次被执行都返回的是一个新的函数引用,所有推荐在组件的构造函数中来绑定this
- static关键字
class TestClass{
constructor(name){
this.name = name;
}
static formatName(name){
return ....;
}
}
console.log(TestClass.formatName('jus'));
ES6 ES5 diff
- 定义方面
1.定义组件
ES5 : 使用React.createClass()
var p = React.createClass({
render:function(){
return (
<Image source={this.props.source}/>
)
}
})
ES6 :通过继承React.Component来定义一个组件类
class P extends React.Component{
render(){
return (
<Image source={this.props.source}/>
);
}
}
- 定义方法
相比ES5 ,ES6在方法定义上语法更加简洁,,给组件定义方法不再用 function()的写法,而是直接用(),在方法的最后也不能有逗号了
ES5:
var p = React.createClass({
test: function(){}
render:function(){
return (
<Image source={this.props.source}/>
)
}
})
ES6 :通过继承React.Component来定义一个组件类
class P extends React.Component{
test(){
}
render(){
return (
<Image source={this.props.source}/>
);
}
}
-
定义组件的属性类型和默认属性
在ES5 ,属性的类型和默认属性分别通过propTypes成员和getDefaultProps方法来实现
ES6 ,可以统一使用static成员来实现
- 在导入与导出的不同
- 导入
ES5 :如果使用CommonJS标准,引入React包基本通过require进行,代码类似这样:
var React = require('react'); // 类似nodejs
var {
Component,
PropTyps,
} = React; // 引用React抽象组件
var AboutPage = require('./app.AboutPage');// 引入app目录下的AboutPage 组件,即为AboutPage.js
ES6: 没有了require,而是使用import来导入组件,有点像Java写法
import React,{
Component,
PropTypes
} from 'react-native'
import AboutPage from './app/AboutPage'
另外,ES6 支持将组件导入作为一个对象,使用‘* as’修饰即可
//引入app 目录下的AboutPage组件作为一个对象,接下来就可使用AboutPage. 来调用AboutPage的方法及属性了
import * as AboutPage from './app/AboutPage'
- 导出
ES5 :module.exports来导出
ES6: export default ....实现相同功能
- 在初始化state上的不同
ES5 : getInitialState 定义
ES6: state = {}
- 在方法作为回调上的不同
React-Native 布局
React-Native 布局方式 => FlexBox(弹性框),是CSS3弹性框布局规范,不是所有浏览器都支持FlexBox,不用考虑
React-Native 按钮 -Touchable系列组件使用详解
React-Native 图片加载技巧 与使用详解
React-Native 程序调试技巧
development manage :iOS:command + d ;Android command + m
refresh: ios: command + r;android : double press R
Enable Live Reload 动态加载 -> 修改完js代码后 界面自动刷新 全量更新
Enable Hot Reloading 热加载 -> 增量更新
实战
TabNavigator 学习
npm install react-native-tab-navigator --save
其中 --save 是为了保证 react-native 项目工程目录下的package.json 中的dependencies 写入当前依赖库的版本号
<TabNavigator.Item
selected={this.state.selectedTab === 'tab_popular'} //触发选中的条件
selectedTitleStyle={{color:'red'}} // tab 文字选中的样式
title="最热"
renderIcon={() => <Image style={styles.image} source={require('./res/images/ic_polular.png')} />} // 默认 icon
renderSelectedIcon={() => <Image style={[styles.image,{tintColor:'red'}]} source={require('./res/images/ic_polular.png')} />}// 选中的icon
badgeText="1" // 消息角标
onPress={() => this.setState({ selectedTab: 'tab_popular' })}> // 按下时 将当前状态设置为tab_popular 刷新组件
react-native 原生Navigator 模块学习
例子: 组件A 跳转到组件B ,并向组件B 传递数据; 然后 组件B 关闭,并回传数据给组件B
// 代码讲解
界面跳转代码逻辑:
this.props.navigator.push({
component:ComponentB ,// 跳转到目标组件
// 传递的参数
params:{
data:'json',
// 回调方法相当于一个桥梁,ComponentB 可以直接使用this.props.onCallback(arguments)改方法进行回调
onCallback:(arguments)=>{
// handle arguments 展示ComponentB 回传过来的数据
}
}
})
ComponentB.js
{this.props.data} 获取传递过来的数据
关闭并回传数据逻辑
onPress={()=>{
this.props.onCallback('json');
this.props.navigator.pop();
}}
<Navigator
initalRoute={{component:ComponentA}}// 默认展示组件
renderScene={(route,navigator)=>{
// 默认组件渲染调用
let Component = route.component;// 从route中取出组件,一定要大写
//将组件返回,并将navigator 传递到默认展示组件中,因为组件A用到了navigator.push
// 如果这里不将延展属性传递给ComponentA, 就会导致ComponentB 在调用onCallback 方法报错,无法将传入的属性复制到组件内
return <Component navigator={navigator} {...route.params}/>
}}
>
</Navigator>
自定义NavigationBar
思路:
- NavigationBar的组成部分{StatusBar} {NavigationContent}=>{LeftView,TitleView,RightView}
- 根据组成部分搭建整体视图组件树
- 对组件树进行分解,根据需求外派出自定义属性,另外通过对子组件的包装()控制组件的样式,需要考虑Android iOS平台的差异性,例如高度、某些组件可能只会在IOS || Android上生效,
- 利用CSS3 flexbox把控整体布局样式
ps : render方法return have only root node ,we can add return()
ListView 组件
相关属性讲解
-
dataSource
列表依赖的数据源,如何定义呢?
let ds = new ListView.DataSource({ //相邻两行进行对比 ,不同则重新渲染,提供高性能的数据处理和访问,类似于adapter rowHasChanged:(r1,r2)=>{ r1 !== r2 } }) this.state = { //cloneWithRows 保证更新datasource数据 dataSource:ds.cloneWithRows(data.result)// data.result ->[t1,t2,...,tn] }x
-
renderRow 绘制行 return 组件视图即可
-
renderSeparator 绘制分割线 return 组件视图即可
-
renderHeader 或者 renderFooter 同理
-
如何实现下拉刷新的效果呢?
RefreshControl 组件为ScrollView 或者ListView 提供下拉刷新的功能 <ListView refreshControl={ <RefreshControl refreshing={this.state.isLoad} // 是否下拉刷新 onRefresh={()=>{()=>this.fetchData}}// 下拉刷新回调 /> } />
Fetch 模块
-
什么是Fetch
Fetch API提供了资源获取(比如通过网络)的接口
React-Native 引入了Fetch -
特性
- 全局的 不需要做额外的导入
启动引导流程
App启动 -> 启动页(读取预配置文件) -> 状态初始化(从服务器获取配置->更新本地数据状态) -> 首页
- app启动 加载React-Native 引擎
- 执行index-OS.js 入口文件
- setup.js 相关组件及服务初始化
- 启动页面加载 注意 :如果用到了setTimeOut方法,记得在组件被销毁的时候clearTimeout(this.timer)
- 首页加载
开源项目TabNavigator学习
-
npm install react-native-tab-navigator --save
-
usage
<TabNavigator> <TabNavigator.Item selected={this.state.selectedTab === 'tab_popular'} selectedTitleStyle={{color:'#2196f3'}} title="最热" renderIcon={() => <Image style={styles.image} source={require('../../../res/images/ic_polular.png')} />} renderSelectedIcon={() => <Image style={[styles.image,{tintColor:'#2196f3'}]} source={require('../../../res/images/ic_polular.png')} />} badgeText="1" onPress={() => this.setState({ selectedTab: 'tab_popular' })}> <PopularPage/> </TabNavigator.Item> //... </TabNavigator>
Popular 模块 实现
由三个部分组成 NavigationToolBar ScrollableTabView(开源项目) ListView
AsyncStorage的基本使用
什么是AsyncStorage?
简单的,异步的,持久化的key-value存储系统,也是React-Native官方推荐的数据
存储方式,旨在代替LocalStorage -
未完待续
实现View滑动的几种方式总结
实现View滑动的几种方式总结
OnLayout方法
layout(getLeft() + offsetX,getTop() + offsetY,getRight() + offsetX,getBottom() + offsetY);
offsetLeftAndRight()与offsetTopAndBottom()
等价于
onLayout
方法
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY); //同时对top和bottom进行偏移
LayoutParams
ViewGrop.LayoutParams lp = getLayoutParams();
lp.leftMargin = getLeft() + offsetX;
lp.rightMargin = getRight() + offsetY;
ScrollTo & ScrollBy & Scroller
这两个方法移动的是View的Content,即调用ScrollXX 移动的是ViewGroup的子View
ScrollBy
scrollBy(dx,dy) 表示视图移动的增量。整个过程可以这样理解:将手机屏幕开成一个盖板,内容视图看成一个画布, 调用scrollBy(20,10),相当于将盖板像X轴右边移动20个像素,Y轴向下移动10个像素。这样看起来 child view 移动恰好X,Y方向相反,
如图:
int offsetX = x - lastX;
int offsetY = y - lastY;
((View)getParent).scrollBy(-offsetX,-offsetY);
ScrollTo
ScrollTo(x,y); 移动到指定坐标位置(x,y)。移动过程同scrollBy类似,都是移动的盖板
Scroller
通过Scroller 实现平滑移动
scroller = new Scroller();
@Override
public void computeScroll(){
super.computeScroll();
if (scroller.computeScrollOffset()) {
((View) getParent).scrollTo(
scroller.getCurrX(),
scroller.getCurrY()
);
// 通过重绘来不断调用computeScroll
invalidate();
}
}
View viewGroup = (View)getParent();
scroller.startScroll(viewGroup.getScrollX(),
viewGroup.getScrollY(),
-viewGroup.getScrollX(),
-viewGroup.getScrollY());
属性动画
ViewDragHelper
Android Api DrawerLayout SlidingPanneLayout 侧边栏滑动效果 ,主要依靠ViewGragHelper ,二ViewGragHelper底层是通过
Scroller ,而Scroller 实现原理和scrollTo 和scrollBy 来实现子View 基本类似
获取坐标值的各种方法
这些方法可以分为如下两个类别
- View提供的获取坐标方法
getLeft()
:获取到的是view左边到其父布局左边的距离;
getTop()
:获取到的是view顶边到其父布局顶边的距离;
getRight()
:获取到的是view右边到其父布局左边的距离;
getBottom()
:获取到的是view底边到其父布局顶边的距离;
- MotionEvent提供的方法
getX()
:获取点击事件距离控件左边的距离,及视图坐标;
getY()
:获取点击事件距离控件顶边的距离,及视图坐标;
getRawX()
:获取点击事件距离整个屏幕左边的距离,及绝对坐标;
getRawY()
:获取点击事件距离整个屏幕顶边的距离,及绝对坐标;
注解日志记录
注解日志记录
注解分类
注解分类分为标准注解和元素注解
-
标准注解
- @OverRide
- @deprecated
- @SuppressWarnings :选择性的取消特定代码段中的警告
- @SafeVarargs: JDK7 新增,用来申明使用了可变长度的参数的方法,其在与泛型类一起使用时不会出现类型安全问题
-
元注解
- @target :定义的注解 使用范围,常见:
@Target(METHOD)
@Target(FIELD)
- @documented: 表示这个注解应该被继承
- @retention:用来声明注解的保留策略,常见:
@Retention(RUNTIME)
运行时注解,jvm会保留注解信息,可以通过反射获取;@Retention(CLASS)
编译时注解注解信息会保留在.java 以及.class中,不会保留在jvm中,通过AbstractProcessor来处理;@Retention(SOURCE)
源码级注解
- @target :定义的注解 使用范围,常见:
定义注解
- 如何定义
-
1.1 情况一
```java @Retention(RetentionPolicy.CLASS or RetentionPolicy.RUNTIME) // 运行时或编译时注解 public @interfaces TestInjector{ // 注解只有成员变量 没有方法,下面的形式即是注解成员变量 String name() default "name"; int age() default 22; } ```
-
1.2 情况二 Retrofit:
```java @Documented @Target(METHOD) // 声明该注解只能方法上使用 @Retention(RetentionPolicy.RUNTIME) // 运行时注解 public @interfaces GET{ String value() default ""; } ```
-
1.3 ButterKnife
@Documented @Target(FIELD) // 声明该注解只能成员变量上使用 @Retention(RetentionPolicy.CLASS) // 编译时注解 public @interfaces BindView{ int value() default ""; }
-
解析注解
运行时解析处理器 : 通过反射获取
public class AnnotationTest{
@GET(value="https://www.baidu.com/")
public void requestApi();
}
public class AnnotationProcessor{
public static void main(String[] args) {
Method[] methods = AnnotationTest.class.getDeclaredMethods();
for (Method m: methods) {
GET get = m.getAnnotation(GET.class);
System.out.println(get.value());
}
}
}
编译时注解处理器
-
分别定义annotations processor java Library
-
编写核心代码ClassProcessor
public class ClassProcessor extends AbstractProcessor{
@override
public boolean process(Set<? extends TypeElement> annotations,RoundEnviroment env){
Messager messager = processingEnv.getMessager();
for (Element element: roundEnv.getElementsAnnotatedWith(BindView.class)) {
if (element.getKind() == ElementKind.FIELD) {
messager.printMessage(Diagnostic.Kind.NOTE,"printMessage:" + element.toString());
}
}
return true;
}
```
-
注册注解处理器
processor 库的main目录下新建resource资源文件夹 接下来再建立META-INF/services 文件夹, 最后在META-INF/services创建 javax.annotation.processing.Processor文件,内容
ClassProcessor
全类名;
获取这使用Google AutoService库也可以,前三步做完就可以应用注解了 -
使用android -apt 插件,我们在项目中使用了processo库,但注解处理器只在便已处理期间需要用到,编译处理完就没有实际用处了,而主工程就添加这个库不必要的文件,为了处理这个问题,引入了apt 插件,两个作用
- 仅仅在编译时期去依赖注解处理器所在的函数库并进行工作,不会打包到apk中
- 为注解处理器生成的代码设置好路径,比便android studio能够很好找到它
线程池和AsyncTask总结
线程池和AsyncTask
线程池的处理流程和原理
AsyncTask
当我们通过主线程去执行耗时的任务,并且在操作完成之后可能还有更新UI时,通常还会用到Handler更新UI线程。虽然实现起来很简单,但是如果有多个任务同时执行时则会显得代码臃肿。Android 提供了AsyncTask,它使得异步任务更加简单
-
onPreExecute()
主线程中执行。一般在执行任务前做准备工作,例如:弹出加载菊花
-
doInBackground(Params... prams)
onPreExecute 方法执行后运行,用来执行耗时操作。在执行过程中可以调用publishProgress(Progress... values)来更新进度信息
-
onProgressUpdate(Progress... values)
在主线程中执行,更新进度信息
-
onPostExecute(Result result)
主线程执行,后台任务执行完毕后回调,doInBackground方法得到结果就是返回的Result的值
源码分析
Android3.0 版本之前的AsyncTask
// 此处源码省略
内部创建了一个ThreadPoolExecutor,核心线程数是5,线程池允许创建最大线程数为128,非核心线程数等待新任务最长时间为1s,采用的阻塞队列是
LinkBlockingQueue
,容量为10。
缺陷:线程池最大的线程数为128 ,加上阻塞队列的10个,所以AsyncTask最大可以容纳138个任务,当提交139个任务时,执行饱和策略,默认抛出RejectedExecutionException
- 并行处理
Android 7.0 版本的AsyncTask
-
WorkRunable
实现了Callable接口,call方法中调用了doInBackground(params),并调用postResult()将结果投递出去 -
FutureTask
是一个可管理异步的任务,它实现了Runable和Future接口,包装Runable和Callable,并提供给Executor执行,workRunable作为参数传递给FutureTask -
SerialExecutor
串行线程池,维护任务队列,将任务串行处理,保证一个时间段只有一个任务执行,而android3.0 之前的版本是并行处理的,所以Android3.0之后的版本不会出现3.0之前的问题,因为线程是一个一个执行的,不会出现超过任务数而执行饱和策略的情况,而如果想要3.0 之后的版本使用并行的线程处理,可以按照如下方案:
asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,"");
asyncTask.executeOnExecutor(Executors.newCachedThreadPool(),"");
Executor exec = new ThreadPoolExecutor(0,Integer.MAX_VALUE,0L, TimeUnit.MILLISECONDS,new LinkedBlockingDeque<Runnable>());
asyncTask.executeOnExecutor(exec,"");
- THREAD_POOL_EXECUTOR
threadPoolExecutor,其核心线程和线程池允许创建的最大线程数都是有CPU的核数来计算出来的,用于处理FutureTask 任务的执行
vim编辑器
- 0 把光标移动到行首
- shift + $ 把光标移到行尾
- vim + 文件名 把光标定位到文件的最后一行
- vim +n 文件名 把光标定位到文件的第n行,如果n大于了文件的总行数,则定位到最后一行
- vim +/bga 文件名 定位到bga在文件中第一次出现的那一行,按n可以跳转到下一次出现的地方
- vim aa bb cc 同时打开多个文件。底行模式下 :n回车表示切换到下一个文件 :N或者:prev回车表示切换到上一个文件
- :ls 列出当前打开的所有文件
- :n 定位到第n行,如果n大于了文件的总行数,则定位到最后一行
- /bga 从光标后一个位置开始,向后搜索,定位到bga第一次出现的地方
- ?bga 从光标后一个位置开始,向前搜索,定位到bga第一次出现的地方
- h 光标左移
- j 光标下移
- k 光标上移
- l 光标右移
- ctrl + f 向下翻页(front)
- ctrl + b 向上翻页(back)
- ctrl + d 向下翻半页(down)
- ctrl + u 向上翻半页(up)
- dd 删除光标所在行
- o 在光标所在行的下方插入一行并切换到输入模式
- yy 复制光标所在的行
- p 在光标所在行的下方粘贴
- P 在光标所在行的上方粘贴
进程启动流程分析
Jenkins-plugin 开发
Jenkins-plugin 开发
本文参考 官方文档
开发前准备项
- 开发环境搭建
- 创建一个
Plugin
项目- 构造、运行
Plugin
项目
-
开发环境搭建
省略
-
创建一个
Plugin
项目mvn -U archetype:generate -Dfilter=io.jenkins.archetypes: -DarchetypeRepository=http://nexus.opendaylight.org/content/repositories/opendaylight.release -DgroupId=XXX -DartifactId=XXX
mvn verify // 校验并下载相关依赖
-
构造、运行
Plugin
项目mvn hpi:run
访问
http://localhost:8080/jenkins/
插件开发
实际需求:在参数化构建时,默认的
String Parameter Plugin
不支持 输入标签textarea
动态从数据源中获取,默认展示配置的defaultValue
,笔者希望在打开构建页面时,就去从指定数据源中读取值并展示在textarea
标签中
大致思路
-
绘制
job
配置界面,确定配置参数 & 绘制Build with Parameters
构建界面,并设置数据源 -
编写代码控制获取逻辑
-
运行
mvn hpi:run
调试 -
发布hpi插件:
mvn clean install
PS:若不执行单元测试相关代码,可以在命令后指定-Dmaven.test.skip=true
-
绘制
job
配置界面,确定配置参数 & 绘制Build with Parameters
构建界面,并设置数据源config.jelly // 此插件在单个Job的配置 <?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:entry title="${%Name}"> // 其中${Name}为引用config.properties定义的字符串常量
<f:textbox field="name"/> // filed指定变量名 Note: 与界面对应的java代码Class成员变量名保持一致
</f:entry>
<f:entry title="${%DefaultValue}">
<f:textbox field="defaultValue"/>
</f:entry>
<f:entry title="${%Description}">
<f:textarea field="description"/>
</f:entry>
<f:entry title="${%VersionCheck}">
<f:checkbox field="isVersionCode"/>
</f:entry>
<f:entry title="${%client}">
<f:checkbox field="whichClient"/>
</f:entry>
</j:jelly>
```jelly
index.jelly // Build with Parameter 构造参数配置
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"
xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<f:entry title="${it.name}" description="${it.description}">
<div name="parameter" description="${it.description}">
// 调用java代码getVersionCode()方法 用于默认数据展示
<f:textbox name="value" value="${it.getVersionCode()}" />
</div>
</f:entry>
</j:jelly>
- 编写代码控制获取逻辑
public class VersionParameterValue extends StringParameterValue {
@DataBoundConstructor
public VersionParameterValue(String name, String value) {
super(name, value);
}
public VersionParameterValue(String name, String value, String description) {
super(name, value, description);
}
@Override
public BuildWrapper createBuildWrapper(AbstractBuild<?, ?> build) {
return super.createBuildWrapper(build);
}
}
// 定义构建参数申明类
public class VersionParameterDefinition extends ParameterDefinition {
private String defaultValue;
private boolean isVersionCode;
private boolean whichClient;
// 构造参数入口 当配置完成 apply之后,这些值将会传入到此类中
@DataBoundConstructor
public VersionParameterDefinition(String name, String description, String defaultValue, Boolean isVersionCode, boolean whichClient) {
super(name, description);
this.defaultValue = defaultValue;
this.isVersionCode = isVersionCode;
this.whichClient = whichClient;
}
public String getVersionCode() {
// 获取gitlab 上 android 项目的versionCode 和versionName
String url = whichClient ? "http://gitlab.xxx.com/dl_dev/xxx/raw/develop/gradle.properties" : "http://gitlab.xxx.com/dl_dev/xxx/raw/develop/gradle.properties";
HttpURLConnection httpCon = null;
InputStream inputStream = null;
BufferedReader br = null;
try {
URL uri = new URL(url);
httpCon = (HttpURLConnection) uri.openConnection();
httpCon.setRequestProperty("PRIVATE-TOKEN", "tAkLzGSbyrhRK8JZkN3b");
httpCon.setRequestMethod("GET");
httpCon.setConnectTimeout(10000);
httpCon.connect();
int responseCode = httpCon.getResponseCode();
String responseMsg = httpCon.getResponseMessage();
System.out.println("responseCode = " + responseCode);
System.out.println("responseMsg = " + responseMsg);
if (responseCode != 200) {
return "please contact wuhaiyang";
}
inputStream = httpCon.getInputStream();
Properties properties = new Properties();
properties.load(inputStream);
String property = isVersionCode ? properties.getProperty("systemProp.versionCode") : properties.getProperty("systemProp.versionName");
return property;
} catch (Exception e) {
e.printStackTrace();
return "please contact wuhaiyang";
} finally {
if (null != httpCon) {
httpCon.disconnect();
}
try {
if (null != inputStream) {
inputStream.close();
}
br.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@CheckForNull
@Override
public ParameterValue getDefaultParameterValue() {
return new VersionParameterValue(getName(), getDefaultValue(), getDescription());
}
@CheckForNull
@Override
public ParameterValue createValue(StaplerRequest staplerRequest, JSONObject jsonObject) {
VersionParameterValue parameterValue = staplerRequest.bindJSON(VersionParameterValue.class, jsonObject);
parameterValue.setDescription(getDescription());
return parameterValue;
}
@CheckForNull
@Override
public ParameterValue createValue(StaplerRequest staplerRequest) {
String[] value = staplerRequest.getParameterValues(getName());
if (null == value || value.length < 1) {
return getDefaultParameterValue();
}
return new VersionParameterValue(getName(), value[0], getDescription());
}
@Extension
public static class DescriptorImpl extends ParameterDescriptor {
/**
* 参数化构建 添加该插件 界面所展示的文案提示
*
* @return
*/
@Override
public String getDisplayName() {
return "Version Auto Fill Parameter";
}
}
}
Gradle 脚本修改Gradle.properties内容
def versionFile = file('gradle.properties')
def Properties versionProps = new Properties()
versionProps.load(new InputStreamReader(new FileInputStream(versionFile), "utf-8"))
def oldVersionName = versionProps['version'].toString()
def isSnap = versionProps['isSnap'].toBoolean()
def versions = oldVersionName.split("\\.")
def one = versions[0].toInteger()
def two = versions[1].toInteger()
def three = versions[2].toInteger()
if (three + 1 == 100) {
two += 1
three = 0
} else {
three += 1
}
if (two == 100) {
one += 1
two = 0
}
def newVersionName = one + "." + two + "." + three
def runTasks = gradle.startParameter.taskNames
if ('uploadArchives' in runTasks) {
versionProps['version'] = newVersionName
versionProps.store(versionFile.newWriter(), null)
}
if (isSnap) {
newVersionName += "-SNAPSHOT"
}
return newVersionName
Android 密钥保护和 C/S 网络传输安全
Android 密钥保护和 C/S 网络传输安全
C/S 网络传输安全
Https握手过程大致流程
-
客户端将自己支持的加密算法类型和检验数据完整性的 HASH 算法类型告诉服务端
-
服务端从客户端传上来的加密算法中选出一种支持的类型,用于生成一对非对称密钥对,并将自己的证书发给客户端,证书中将带有这对非对称密钥的公钥和证书颁发机构、过期时间等。其中所谓的非对称加密及其公钥和密钥,如果不懂,可以简单理解为:这是一种加密算法,私钥加密的内容只有公钥才能解密,反之公钥加密的内容只有私钥才能解密,以此来保证两端信息的安全性
-
客户端获得证书后,会对证书的合法性进行检验,如果证书合法,则客户端将随机生成一对称加密的密钥,并使用服务端给的非对称加密密钥对这个对称加密密钥进行加密,并生成 HASH 值,统一发给服务端。所谓对称加密及其密钥,简单说:这是一种加密算法,加密和解密使用的密钥是一样的
-
服务端拿到信息后,使用私钥进行解密取出对称加密的密钥,并验证 HASH 值。验证无误后,使用这个对称加密密钥对握手信息进行加密,发给客户端
-
客户端解密和 HASH 验证,无误则握手成功完成。接下来所有的通讯都会使用这个已经同步到两端的对称加密密钥进行加密通讯。而一旦这个握手过程中有任何错误,都会中止握手过程,请求的参数和内容传输是在这些过程之后,因此若是握手过程出错,则不会发送请求内容。
证书相关(SSL,X.509,PEM,DER...)
证书标准
- X.509
一种证书标准,主要定义了证书中应该包含哪些内容,其详情可以参考RFC5280,SSL使用的就是这种证书标准
编码格式
-
PEM
Privacy Enhanced Mail,打开看文本格式,以"-----BEGIN..."开头, "-----END..."结尾,内容是BASE64编码
Apache和*NIX服务器偏向于使用这种编码格式. -
DER
Distinguished Encoding Rules,打开看是二进制格式,不可读Java和Windows服务器偏向于使用这种编码格式.
相关文件扩展名
这是比较误导人的地方,虽然我们已经知道有PEM和DER这两种编码格式,但文件扩展名并不一定就叫"PEM"或者"DER",常见的扩展名除了PEM和DER还有以下这些,它们除了编码格式可能不同之外,内容也有差别,但大多数都能相互转换编码格式.
-
CRT
CRT应该是certificate的三个字母,其实还是证书的意思,常见于*NIX系统,有可能是PEM编码,也有可能是DER编码,大多数应该是PEM编码 -
CER
还是certificate,还是证书,常见于Windows系统,同样的,可能是PEM编码,也可能是DER编码,大多数应该是DER编码 -
CSR
即证书签名请求,这个并不是证书,而是向权威证书颁发机构获得签名证书的申请,其核心内容是一个公钥(当然还附带了一些别的信息),在生成这个申请的时候,同时也会生成一个私钥,私钥要自己保管好.做过iOS APP的朋友都应该知道是怎么向苹果申请开发者证书的吧
Android 密钥保护
使用 so 库存储预设 key / secret,使用 Android KeyStore 存储运行时动态获取到的私密内容。
Linux 常用命令
面试流程 & 原创面试题
日常dlzdd 、dljxs 发版checkList
dlzdd checkList
- 检查gradle.properties以下几项
systemProp.versionCode=15
systemProp.versionName=2.5.9
systemProp.environment 值为release
release_umeng_key是否正确
systemProp.push_debug = false 特别注意
release_webservice_url 、release_msgcenter_url 配置是否正确
- 检查app目录下的build.gradle
andResGuard{
use7zip = false
}
buildType{
release {
manifestPlaceholders = [UMENG_APPKEY: release_umeng_key]
signingConfig signingConfigs.release
minifyEnabled true
zipAlignEnabled true
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
productFlavors {
Danlu {}
Market360 {}
TecentMarket {}
PPHelper {}
}
- msg-center build.gradle
systemProp.push_debug = false 特别注意 ->打包之后改为true
- 相关代码
GlobalApiAgent.Java
.isLogData(false)
dljxs checkList
- 检查gradle.properties以下几项
systemProp.versionName=1.1.8
systemProp.versionCode=10
release_umeng_key 是否正确
systemProp.push_debug = false
release_webservice_url 、release_msgcenter_url 配置是否正确
- build.gradle
andResGuard{
use7zip = false
}
release {
manifestPlaceholders = [UMENG_APPKEY: release_umeng_key]
buildConfigField 'boolean', 'push_debug', 'false'
buildConfigField 'String', 'webservice_url', release_webservice_url
buildConfigField 'String', 'msgcenter_url', release_msgcenter_url
signingConfig signingConfigs.release
minifyEnabled true
zipAlignEnabled true
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
- GlobalApiAgent
.baseUrl(msgUrl)
.interfaceApi(DanluMessageSystemApi.class)
.isLogData(false)
2017 校招常考算法题归纳 & 典型题目汇总
Lifecycle-library-stable
Google 最近发布了稳定
Lifecycle
library。笔者认为:如果你正在使用alpha或者beta版本,以下内容可能对你有用。
Deprecated LifecycleActivity
在stable version 1.0.0 之前,LifeCycle Library 并没有实现 LifecycleOwner
这个接口,所以我们不得不让我们项目XXXActivity继承LifecycleActivity。笔者认为这样有一个毛病:该死的侵入性
但是,在stable version 1.0.0 之后,Google可能意识到这个问题,AppCompatActivity 已经实现了LifecycleOwner
接口,这样就可以直接继承原生的AppCompatActivity,然后使用它给你带了的功能体验
AppCompatActivity be good for you?
我按照官方文档建议,替换LifecycleAvtivity ,使用AppCompatActivity,代码如下:
public class BaseActivity extends AppCompatActivity{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState){
super.onCreate(savedInstanceState);
getLifecycle().addObserver(presenter);
}
}
笔者提出几点注意事项
- support:appcompat-v7 版本一定要>=26.1.0。27.0.2 对应的lifecycle version == 1.0.3
- presenter 必须实现
LifecycleObserver
接口
Deprecated LifecycleRegistryOwner
如果你想自定义Lifecycle owner ,你应该实现 LifecycleOwner
,而不是LifecycleRegistryOwner
Just Demo
- build.gradle
appcompat 26.1.0 依旧使用的stable version 1.0.0, 可以考虑手动添加最新版本。 笔者尝试使用exclude 将appcompat中的lifecycle去掉,发现很多库(eg:recyclerview constrait-layout fragment ...)都依赖Lifecycle, 笔者便放弃了
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation "android.arch.lifecycle:runtime:1.0.3"
annotationProcessor "android.arch.lifecycle:compiler:1.0.3"
- Activity
public class BaseActivity extends AppCompatActivity{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState){
super.onCreate(savedInstanceState);
getLifecycle().addObserver(presenter);
}
}
- Observer
public class Presenter implements LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_ANY)
public void onAny(LifecycleOwner owner, Lifecycle.Event event) {
Log.w("@@@@ L41", "Presenter onAny -> " + "");
}
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
public void onCreate() {
Log.w("@@@@ L41", "Presenter onCreate -> " + "");
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void onPause() {
Log.w("@@@@ L41", "Presenter onPause -> " + "");
}
}
热修复与插件化专题一:dex&虚拟机&ClassLoader
热修复与插件化专题专题一:dex&虚拟机&ClassLoader
前期预备知识
dex/class
- class文件结构解析
-
什么是class文件?
能够被jvm识别,加载并执行的文件格式
-
如何生成一个class文件
- 通过IDE自动帮我们build
- 手动通过javac 去生成class文件
PS: 如何制定jdk版本生成字节码文件呢?—>javac -target 1.6 -source 1.6 HelloWorld.java - 通过java命令去执行class文件
java com.example.hostfit.Test ps: 执行时根据全类名来执行的
-
class文件的作用
记录类文件的所有信息(记录了this super 等关键字) -
- 一种八位字节的二进制流文件
- 各个数据按照顺序紧密排列,无间隙
- 每个类或接口都单独占一个class文件
-
class文件弊端
- 内存占用大,不适合移动端
- 堆栈加载模式,加载速度慢
- 文件IO操作多,类查找慢
-
- dex文件结构解析
-
什么是dex文件
被DVM虚拟机识别,加载并执行的文件格式
-
如何生成dex文件
- 通过IDE自动帮我们build生成
- 手动通过dx命令去生成dex文件
javac -target 1.6 -source 1.6 Test.java
生成class文件。ps:指定1.6 版本为了保证兼容dx --dex --output=Test.dex com/example/hostfit/Test.class
ps: 是根据全类名来找的
- 手动运行dex文件在手机上
- 将dex文件push 到手机存储卡中
adb push Test.dex '手机路径(例如:/sdcard)'
dalvikvm -cp Test.dex com.example.hostfit.Test
ps: 注意全类名
- 将dex文件push 到手机存储卡中
-
dex文件的作用
记录整个工程中所有类文件的信息。
-
- 一种八位字节的二进制流文件
- 各个数据按照顺序紧密排列,无间隙
- 整个应用中所有的java源文件都放在一个dex文件中 ps: 不考虑android 官方提供的multidex
-
- class 与 dex 文件对比
- 本质上他们都是一样的,dex文件从class文件演变而来
- class文件存在许多冗余信息(一个类就有一个常量池),而dex文件会去除冗余,并进行整合
jvm/dvm/art 三种虚拟机
-
java 虚拟机结构解析
- jvm整体结构
-
java代码的编译和执行过程
-
Java 内存管理
-
Java 栈区
用来存放Java方法执行的所有数据 ps: method call-> a -> b - c;栈区由栈帧组成,一个栈帧代表一个方法的执行。
那什么是栈帧呢?每一个方法从调用到执行完成就对应一个栈帧在虚拟机中入栈到出栈。每一个栈帧包括局部变量表、栈操作数、动态链接、方法出口。例如(StackOverFlow异常)
本地方法栈:和Java方法栈如出一辙,只不过本地方法栈是专门未Native方法服务的
-
方法区
存储被虚拟机加载的类信息,常量,静态变量,即时编译器后等数据,用于占据内存的
-
Java堆
所有通过New创建的对象的内存都在堆中分配,是虚拟机中最大的一块内存,是GC 要回收的部分
- Young Generation : 刚刚New出来的对象
- Old Generation: 当Young Generation 中内存空间不足的时候,就会将Young Generation 中的对象按照一定的算法、规则等存放到Old Generation, 这样 Young Generation 就可以继续分配内存,当两者都没有剩余的空间的时候,就会发生OOM异常,垃圾回收器主要针对这两块内存区域,
- Permanent Generation: Java8 已经移除
特点:Young Old Generation 可以动态分配,当我们的服务器处理的是及时通讯相关服务,就可以将Young Generation内存区域调整大一些;当我们不需要频繁去创建对象的时候,可以将Young Generation 内存区域调整小一些,这样达到内存对象常驻的效果
-
-
Java 内存回收机制
-
Dalvik 与 jvm的不同
- 执行的文件不同 一个是dex 一个是class文件
- 类加载的系统(ClassLoader)与JVM的区别比较大
- jvm 只能同时存在一个,DVM可以同时存在多个
- DVM 是基于寄存器的(运行更快),JVM 是基于栈的
-
ART比Dalvik有哪些优势
- DVM 使用的是JIT来将字节码转换成机器码(每次运行),效率低
- ART 是采用AOT的预编译技术(安装的时候就将字节码转换成机器码存储于介质中,不需要每次进行转换),执行速度更快
- ART 会占用更多的应用安装时间和存储空间(以空间换时间)
class loader(Java Android)
类是如何加载到虚拟机的?
-
Java 中的ClassLoader回顾
-
Android中ClassLoader 作用详解
-
Android ClassLoader的种类
-
BootsClassLoader
用来加载Android framework层的一些dex文件
-
PathClassLoader
用来加载已经安装到系统中的apk文件中的dex文件
-
DexClassLoader
用来加载指定目录中dex文件
-
BaseDexClassLoader
是PathClassLoader DexClassLoader 的父类
一个App至少需要BootClassLoader 和PathClassLoader
-
-
Android ClassLoader 的特点
-
双亲代理模型特点
当前的classLoader去加载此类,如果当前此类已经被ClassLoader加载过就不再加载,直接返回;
如果未加载,便会查询它的Parents 是否加载过此类,如果加载过 就返回parents加载过的字节码文件;
如果整个继承线的都没有加载过此类,便会子类真正的加载,提高类加载效率。这样就会带来以下两个作用 -
类加载的共享功能
一些FrameWork层级的类 ,一旦被顶层的classLoader加载过,那么它就会缓存到内存里面,以后任何地方用到,就不用重新加载了 -
类加载的隔离功能
不同继承路线上的ClassLoader 加载的类不是同一个类,避免开发者自己写一代码伪造成系统的类库来访问我们系统可见成员变量。例如:系统层级的类一般初始化的时候就会加载,比如java.lang.String, 应用程序启动之前就会被系统加载,如果在一个app里面写一个自定String 替换掉java.lang.String 会造成严重的安全问题。判断是否是同一个类判断,除了className packageName 另外还需要是同一个ClassLoader加载的。
-
-
ClassLoader 源码 (加载流程)
ClassLoader loadClass 首先判断被自己或者双亲加载过,如果未加载过,调用BaseDexClassLoader 的findClass,调用DexPathList findClass ,并且完成将dex文件转换成DexFile ,转换成Elements, 遍历数据,调用DexFile loadClassBinaryName - > native
-
-
Android 中动态加载要点?
- 有许多组件(Activity)类需要注册才能使用
- 资源动态加载复杂(注册、兼容性)
- 程序运行的时候需要一个上下文环境
热修复
稍等片刻,美味敬请期待~~~
CSS 相关总结
EffectiveMac
CountDownTimer 不能倒计时到0的一种解决思路
-
拷贝CountDownTimer源码 重新
if (millisLeft < mCountdownInterval) {
// no tick, just delay until done
onTick(millisLeft); // 在源码中加上这一句
sendMessageDelayed(obtainMessage(MSG), millisLeft);
} -
自定义CountTimerButton源码中
/**- CountDownTimer Android的倒计时器;
*/
private FixCountDownTimer timer = new FixCountDownTimer(60 * 1000, 1000) {
@OverRide
public void onTick(long millisUntilFinished) {
int ceil = (int) Math.ceil(millisUntilFinished * 1.0 / 1000); // 向上取整
setText( ceil + "秒后重新获取");
}@Override public void onFinish() { Log.w("@@@@ L75", "CountTimerButton:onFinish() -> " + "onFinish"); setText("获取短信验证码"); //设置为显示重新获取验证码; setEnabled(true); //空件设置为可用; if (null != mInnerClickListener) { mInnerClickListener.onCountFinish(); } }
};
- CountDownTimer Android的倒计时器;
Jenkins构建Android项目持续集成
title: jenkins + android 自动打包工具实现方案
date: 2013/7/13 20:46:25
嘿,哥们,帮我打一个Uat分支的安装包....,十分钟之后发给我!!!. 终于摆脱打包的尴尬了
标签(空格分隔): 未分类
相关介绍
Jenkins本身是一个开源持续集成平台(这里不过多介绍)。谈到这里,很多人会联想到:持续集成、持续交互、持续部署等专业名词。下面谈谈个人的理解:
假设我们把开发工作流程分为以下几个阶段:
编码->构建->集成->测试->交互->部署
持续集成:
个人开发的功能模块想软件整体部分交互,频繁进行集成以便于更快的发现其中的错误,这个概念来源于极限编程(XP)
CI需要具备:
- 全面化自动测试
- 灵活的基础设施
- 版本控制工具
- 自动化的构建和软件发布的流程的工具(Jenkins ,Flow.CI)
- 反馈机制
持续集成,该如何入手
最重要的还是选择合适的持续集成系统。搭建私有部署还是托管型持续集成系统,视情况而定,简单对比下:
- Self Hosted CI 是将软件部署到公司机房或内网中,需要不同机器之间进行环境配置,灵活
- HostedCI 由Saas型的CI服务 ,不考虑装机器、软件、环境搭建成本。常见有CiricleCI,CodeShip TravisCI等,国内最新的持续集成服务有Flow.ci
持续交互:
持续交互是在持续集成基础之上,将集成后的代码部署到更加贴近真实运行环境中,以便于更早发现代码中相关bug
持续部署:
持续部署是指当交互代码通过评审之后,自动部署到生产环境中,指持续交互的最高阶段。可以相对独立的部署新的功能
回归主题
总体步骤:
- 搭建jenkins持续集成环境 并挂载到tomcat上,在运行服务器上配置gradle android sdk python jdk 等必要环境变量
- 访问jenkins,安装必要的插件(git、gradle.....)
- 创建项目并配置 根据个人需求配置相关构建参数 ,远端代码路径、构建脚本、上传生成的产物等
- 根据jenkins上设置的相关构建参数,在工程代码中修改gradle 脚本 来进行参数上的对接与赋值。
具体实现方案
基础环境搭建
- 准备:JDK环境 ,tomcat环境,Maven环境,jenkins.war包;
- 在linux上安装JDK,tomcat和maven,这里就不赘述了
- 将jenkins.war包放置在/tomcat/webapps下
- 然后启动tomcat,./startup.sh & tail -f ../logs/catalina.out
- 启动成功之后,访问http://ip:8080(端口配置)/jenkins
效果图p-1:
安装jenkins用到的插件
傻瓜式安装过程,进入:
系统管理->插件管理->管理插件->可选插件 勾选安装
Branch API Plugin
build timeout plugin
build-name-setter
Credentials Binding Plugin
description setter plugin
Dynamic Parameter Plug-in
Environment Injector Plugin
fir-plugin(可选)
Git plugin(可选)
GIT server Plugin(可选)
Gradle Plugin
Pipeline: Basic Steps
Pipeline: Build Step
Pipeline: Input Step
Pipeline: Nodes and Processes
Pipeline: Stage Step
Post-Build Script Plug-in
SSH Slaves plugin
Subversion Release Manager plugin(可选)
Timestamper
Workspace Cleanup Plugin
Subversion Plug-in(可选)
....
根据自己需求进行插件的安装
在jenkins平台上创建项目并配置相关参数
以上选择的参数都是个人项目需求定制的,作为参考而已
其中Invoke Gradle script用来指定gradle 版本,笔者as项目用的gradle 版本为2.14.1(指定之前需要到jenkins系统设置去设置gradle路径);gradle clean assemble'${product_flavors}${environment}' resguard,其中${variableName}是指 引用之前的参数化构建的参数名对应的值,那么组合起来意思就是:打指定渠道下 & 项目build.gradle 指定buildType任务的包 并且运行resguard(微信的资源压缩方案)任务.
保存配置并apply
按捺不住跃跃一试
选择Build with Parameters ,选择相关参数进行构建,此时到控制台输出中查看一些关键性日志输出
/data/gradle/gradle-2.14.1/bin/gradle -Denvironment=dl_so -Dcustome_webservice_url=XXXXX -Dcustome_msgcenter_url=XXXXX -Dpush_debug=true -Dcus_version_name=2.5.3 -Dumeng_debug=true -Dbuild_branch=aw_feature_androidzdd_pandora -Dcus_version_code=9 -Dproduct_flavors=Market360 -Dconfusion=true clean assembleMarket360dl_so resguard
是不是有一种似曾相似的赶脚,我们对其中进行关键性的提取:
gradle -D${ variableName1}=? -D${variableName2}=? ... clean assembleMarket360dl_so resguard
clean 后面的上文已经解释过了,这个地方不再赘述;-D 是什么意思呢?说白了就是Gradle 想虚拟机传参。举例说明:
- 在项目根目录下的gradle.properties创建一个系统属性
systemProp.testparams = "123"
接着更改住module app 下的build.gradle 脚本中添加一个gradle 任务:
task justrun << {
println System.properties['testparams']
}
调出cmd, 运行gradle -Dtestparams=XXXX justrun ,数秒中之后,你会看cmd下看到 XXXX的输出 说明整个传值过程走通!
2. 如果我们在gradle.properties中这样申明我们的参数:
testparams = "123"
build.gradle :
task justrun << {
println testparams
}
cmd: gradle -Dorg.gradle.project.testparams=XXXX justrun
必须得加上前缀:org.gradle.project.这样传参才有效!
搞懂了这些,下一步我们尝试去修改项目gradle脚本了...
项目gradle脚本改写
有如下需求:测试人员可以选择常见的几套环境(服务器地址)进行构建,并且可以手动输入地址(优先级最高)进行构建。
开始操刀
接下来我们在项目gradle.properties 添加以下参数:
systemProp.custome_webservice_url =
systemProp.custome_msgcenter_url =
以及
uat_webservice_url = "http://XXX.57.216.XXX:9003"
uat_msgcenter_url = "http://XXX.92.162.XXX:9000/"
test1_webservice_url = "http://XXX.56.70.XXX:9003"
test1_msgcenter_url = "http://XXX.200.123.XXX:9001/"
develop_webservice_url = "http://XXX.196.101.XXX:8002"
develop_msgcenter_url = "http://XXX.196.101.XXX:8001"
....
app Module 下 的build.gradle 中部分脚本:
defaultConfig{
applicationId "XXXXXX"
minSdkVersion 15
targetSdkVersion 21
versionCode parseVersionCode()
versionName joinQuotes()
multiDexEnabled true
buildConfigField 'String', 'webservice_url', develop_webservice_url
buildConfigField 'String', 'msgcenter_url', develop_msgcenter_url
dl_so {
buildConfigField 'String', 'webservice_url', getJenkinsInputUrl(true, test1_webservice_url)
buildConfigField 'String', 'msgcenter_url', getJenkinsInputUrl(false, test1_msgcenter_url)
.....
}
def getJenkinsInputUrl(boolean isWebserviceUrl, String url) {
def res = url
if (isWebserviceUrl) {
def cwu = System.properties['custome_webservice_url']
if (null != cwu && !"\"\"".equals(cwu) && !"".equals(cwu)) {
res = cwu
}
} else {
def cmu = System.properties['custome_msgcenter_url'];
if (null != cmu && !"\"\"".equals(cmu) && !"".equals(cmu)) {
res = cmu
}
}
if (res == null || "\"\"".equals(res) || "".equals(res)) {
res = "http://app.baidu.com"
}
if (!res.contains("\"")) {
res = "\"" + res + "\""
}
return res
}
}
如果jenkins动态传入的相关参数不为空,那么buildConfigFiled 对应的参数值就为动态传入的参数值。
最后在java代码中动态去获取BuildConfig.XXX
python 上传脚本
#coding: utf-8
import os
import re
import requests
import sys
if len(sys.argv) == 2:
mainpath = sys.argv[-1]
else:
exit()
temppath = '/data/temp/Temp1.apk'
cmd = 'curl -F "file=@{path}" -F "uKey=XXXX" -F "_api_key=XXX" http://www.pgyer.com/apiv1/app/upload'
content = os.popen(cmd.format(path=mainpath)).read()
print(content)
match = re.search('"appKey":"(\w+)"', content)
if match:
appkey = match.group(1)
content = os.popen(cmd.format(path=temppath)).read()
url = 'http://static.pgyer.com/' + appkey
html = requests.get(url).text
match = re.search('http://static.pgyer.com/app/qrcodeHistory/\w+', html)
if match:
print('appKey#{soonAppkey}#appKeysoon#{soon}#soon'.format(soonAppkey=appkey,soon=match.group()))
else:
print('no qrcode')
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.